diff options
208 files changed, 4248 insertions, 1295 deletions
diff --git a/.github/workflows/gi-docs.yml b/.github/workflows/gi-docs.yml index d0ac9b8..565b6d5 100644 --- a/.github/workflows/gi-docs.yml +++ b/.github/workflows/gi-docs.yml @@ -44,7 +44,8 @@ jobs: - name: Clean and Copy Files run: | rm -rf dist/libastal - cp -r src/result/libastal dist/libastal + mkdir dist/libastal + cp -r src/result/* dist/libastal - name: Push to Pages Repo run: | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7b29bcf..8b4ea71 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,7 +5,7 @@ You can contribute by: - [Suggesting new features](https://github.com/Aylur/astal/issues/new?assignees=&labels=enhancement&projects=&template=feature_request.md&title=) - [Reporting bugs](https://github.com/Aylur/astal/issues/new?assignees=&labels=bug&projects=&template=bug_report.md&title=) - Improving docs with additional contexts and examples - - adding more distros to sections about installations e.g [bulding from source](https://aylur.github.io/astal/guide/getting-started/installation#bulding-libastal-from-source) + - adding more distros to sections about installations e.g [building from source](https://aylur.github.io/astal/guide/getting-started/installation#building-from-source) - Adding more example projects to [examples](https://github.com/Aylur/astal/tree/main/examples) - Adding new language support/binding. For these open a PR for discussions. - Adding new libraries e.g support for more wayland compositors diff --git a/docs/default.nix b/docs/default.nix index 3eae600..5c7a8c7 100644 --- a/docs/default.nix +++ b/docs/default.nix @@ -2,239 +2,15 @@ self, pkgs, }: let - inherit (builtins) replaceStrings readFile; - readVer = file: replaceStrings ["\n"] [""] (readFile file); + inherit (builtins) removeAttrs concatStringsSep map attrValues; + packages = attrValues (removeAttrs self.packages.${pkgs.system} ["default" "docs" "gjs"]); - toTOML = (pkgs.formats.toml {}).generate; + cp = pkg: '' + doc="${pkg.doc}/share/doc" + name=$(ls $doc) - docgen = pkgs.gi-docgen.overrideAttrs { - patches = [../nix/doc/gi-docgen.patch]; - }; - - genLib = { - flakepkg, - gir, - version, - description, - api-ver ? "0.1", - authors ? "Aylur", - dependencies ? {}, - out ? "libastal/${flakepkg}", - browse ? flakepkg, - website ? flakepkg, - }: let - name = "Astal${gir}-${api-ver}"; - src = self.packages.${pkgs.system}.${flakepkg}.dev; - - data = toTOML gir { - library = { - inherit description authors; - version = readVer version; - license = "LGPL-2.1"; - browse_url = "https://github.com/Aylur/astal/tree/main/lib/${browse}"; - repository_url = "https://github.com/aylur/aylur.git"; - website_url = "https://aylur.github.io/astal/guide/libraries/${website}"; - dependencies = ["GObject-2.0"] ++ (builtins.attrNames dependencies); - }; - - extra.urlmap_file = "urlmap.js"; - dependencies = {inherit (dependency) "GObject-2.0";} // dependencies; - }; - in '' - mkdir -p $out/${out} - cat ${urlmap} > urlmap.js - gi-docgen generate -C ${data} ${src}/share/gir-1.0/${name}.gir - cp -r ${name}/* $out/${out} - ''; - - dependency = { - "GObject-2.0" = { - name = "GObject"; - description = "The base type system library"; - docs_url = "https://docs.gtk.org/gobject/"; - }; - "Gtk-3.0" = { - name = "Gtk"; - description = "The GTK toolkit"; - docs_url = "https://docs.gtk.org/gtk3/"; - }; - "Gtk-4.0" = { - name = "Gtk"; - description = "The GTK toolkit"; - docs_url = "https://docs.gtk.org/gtk4/"; - }; - "AstalIO-0.1" = { - name = "AstalIO"; - description = "Astal Core library"; - docs_url = "https://aylur.github.io/libastal/io"; - }; - "NM-1.0" = { - name = "NetworkManager"; - description = "The standard Linux network configuration tool suite"; - docs_url = "https://networkmanager.dev/docs/libnm/latest/"; - }; - "WP-0.5" = { - name = "WirePlumber"; - description = "Modular session/policy manager for PipeWire"; - docs_url = "https://pipewire.pages.freedesktop.org/wireplumber/"; - }; - }; - - urlmap = pkgs.writeText "urlmap" '' - baseURLs = ${builtins.toJSON [ - ["GLib" "https://docs.gtk.org/glib/"] - ["GObject" "https://docs.gtk.org/gobject/"] - ["Gio" "https://docs.gtk.org/gio/"] - ["Gdk" "https://docs.gtk.org/gdk3/"] - ["Gtk" "https://docs.gtk.org/gtk3/"] - ["GdkPixbuf" "https://docs.gtk.org/gdk-pixbuf/"] - ["AstalIO" "https://aylur.github.io/libastal/io"] - - # FIXME: these are not gi-docgen generated, therefore links are broken - ["NM" "https://networkmanager.dev/docs/libnm/latest/"] - ["WP" "https://pipewire.pages.freedesktop.org/wireplumber/"] - ]} + mkdir -p "$out/$name" + cp -r "$doc/$name" $out ''; in - pkgs.stdenvNoCC.mkDerivation { - name = "reference"; - src = ./.; - - nativeBuildInputs = with pkgs; [ - docgen - glib - json-glib - gobject-introspection - gtk3 - gtk4 - gtk-layer-shell - gtk4-layer-shell - gdk-pixbuf - libdbusmenu-gtk3 - wireplumber - networkmanager - self.packages.${system}.io - ]; - - installPhase = '' - runHook preInstall - ${genLib { - flakepkg = "io"; - gir = "IO"; - api-ver = "0.1"; - browse = "astal/io"; - description = "Astal Core library"; - version = ../lib/astal/io/version; - }} - ${genLib { - flakepkg = "astal3"; - gir = ""; - api-ver = "3.0"; - browse = "astal/gtk3"; - description = "Astal GTK3 widget library"; - version = ../lib/astal/gtk3/version; - dependencies = {inherit (dependency) "AstalIO-0.1" "Gtk-3.0";}; - }} - ${genLib { - flakepkg = "astal4"; - gir = ""; - api-ver = "4.0"; - browse = "astal/gtk4"; - description = "Astal GTK4 widget library"; - version = ../lib/astal/gtk4/version; - dependencies = {inherit (dependency) "AstalIO-0.1" "Gtk-4.0";}; - }} - ${genLib { - flakepkg = "apps"; - gir = "Apps"; - description = "Application query library"; - version = ../lib/apps/version; - }} - ${genLib { - flakepkg = "auth"; - gir = "Auth"; - authors = "kotontrion"; - description = "Authentication using pam"; - version = ../lib/auth/version; - }} - ${genLib { - flakepkg = "battery"; - gir = "Battery"; - description = "DBus proxy for upowerd devices"; - version = ../lib/battery/version; - }} - ${genLib { - flakepkg = "bluetooth"; - gir = "Bluetooth"; - description = "DBus proxy for bluez"; - version = ../lib/bluetooth/version; - }} - ${genLib { - flakepkg = "cava"; - gir = "Cava"; - description = "Audio visualization library using cava"; - version = ../lib/cava/version; - authors = "kotontrion"; - }} - ${genLib { - flakepkg = "greet"; - gir = "Greet"; - description = "IPC client for greetd"; - version = ../lib/greet/version; - }} - ${genLib { - flakepkg = "hyprland"; - gir = "Hyprland"; - description = "IPC client for Hyprland"; - version = ../lib/hyprland/version; - }} - ${genLib { - flakepkg = "mpris"; - gir = "Mpris"; - description = "Control mpris players"; - version = ../lib/mpris/version; - }} - ${genLib { - flakepkg = "network"; - gir = "Network"; - description = "NetworkManager wrapper library"; - version = ../lib/network/version; - dependencies = {inherit (dependency) "NM-1.0";}; # FIXME: why does this not work? - }} - ${genLib { - flakepkg = "notifd"; - gir = "Notifd"; - description = "Notification daemon library"; - version = ../lib/notifd/version; - }} - ${genLib { - flakepkg = "powerprofiles"; - gir = "PowerProfiles"; - description = "DBus proxy for upowerd profiles"; - version = ../lib/powerprofiles/version; - }} - ${genLib { - flakepkg = "river"; - gir = "River"; - description = "IPC client for River"; - version = ../lib/river/version; - authors = "kotontrion"; - }} - ${genLib { - flakepkg = "tray"; - gir = "Tray"; - description = "StatusNotifierItem implementation"; - version = ../lib/tray/version; - authors = "kotontrion"; - }} - ${genLib { - flakepkg = "wireplumber"; - gir = "Wp"; - description = "Wrapper library over the wireplumber API"; - version = ../lib/wireplumber/version; - authors = "kotontrion"; - dependencies = {inherit (dependency) "WP-0.5";}; # FIXME: why does this not work? - }} - runHook postInstall - ''; - } + pkgs.runCommand "docs" {} (concatStringsSep "" (map cp packages)) diff --git a/docs/guide/getting-started/installation.md b/docs/guide/getting-started/installation.md index e32b6a9..0cda3c5 100644 --- a/docs/guide/getting-started/installation.md +++ b/docs/guide/getting-started/installation.md @@ -22,22 +22,22 @@ maintainer: [@Aylur](https://github.com/Aylur) Read more about it on the [nix page](./nix#astal) -## Bulding From Source +## Building From Source 1. Install the following dependencies :::code-group ```sh [<i class="devicon-archlinux-plain"></i> Arch] -sudo pacman -Syu meson vala gtk3 gtk-layer-shell gobject-introspection +sudo pacman -Syu meson vala valadoc gtk3 gtk-layer-shell gobject-introspection ``` ```sh [<i class="devicon-fedora-plain"></i> Fedora] -sudo dnf install meson vala valadoc gtk3-devel gtk-layer-shell-devel gobject-introspection-devel +sudo dnf install meson vala valadoc gtk3-devel gtk-layer-shell-devel gobject-introspection-devel wayland-protocols-devel ``` ```sh [<i class="devicon-ubuntu-plain"></i> Ubuntu] -sudo apt install meson valac libgtk-3-dev libgtk-layer-shell-dev gobject-introspection libgirepository1.0-dev +sudo apt install meson valac valadoc libgtk-3-dev libgtk-layer-shell-dev gobject-introspection libgirepository1.0-dev ``` ::: diff --git a/docs/guide/getting-started/introduction.md b/docs/guide/getting-started/introduction.md index 782c069..43a7bd8 100644 --- a/docs/guide/getting-started/introduction.md +++ b/docs/guide/getting-started/introduction.md @@ -2,13 +2,15 @@ ## What is Astal? -Astal (_meaning "desk"_) is a suite of libraries in Vala and C. +Astal (_meaning "desk"_) is a suite of libraries written in Vala and C. The core library [astal3](https://aylur.github.io/libastal/astal3) and -[astal4](https://aylur.github.io/libastal/astal4) (not yet available) -has some Gtk widgets that come packaged, +[astal4](https://aylur.github.io/libastal/astal4) +have some Gtk widgets that come packaged, the most important one being the [Window](https://aylur.github.io/libastal/astal3/class.Window.html) which is the main toplevel component using [gtk-layer-shell](https://github.com/wmww/gtk-layer-shell). This is what allows us to use Gtk as shell components on Wayland. -The other part of the core library [astal-io](https://aylur.github.io/libastal/astal-io) +The other component is [Application](https://aylur.github.io/libastal/astal3/class.Application.html) +which provides a way to send messages from the cli to running Astal instances. +The other part of the core library is [astal-io](https://aylur.github.io/libastal/astal-io) which contains some utility GLib shortcut for running external processes, reading, writing and monitoring files, timeout and interval functions. @@ -23,4 +25,4 @@ or an applauncher, but gave up because writing a workspace widget, implementing the notification daemon or handling a search filter was too much of a hassle? Astal libraries have you [covered](../libraries/references#astal-libraries), you don't have to worry about these, -you just define the layout, style it with CSS and that's it. +you just define the layout, style with CSS hook up the state from libraries you want and that's it. diff --git a/docs/guide/getting-started/nix.md b/docs/guide/getting-started/nix.md index 6bc5d9b..3ded888 100644 --- a/docs/guide/getting-started/nix.md +++ b/docs/guide/getting-started/nix.md @@ -5,7 +5,43 @@ next: --- # Nix -Using Astal on Nix will require you to package your project. +Using Astal on Nix will require you to write a derivation for your project. +You can either copy and build off of these example flakes or you can +incorporate the derivations into your existing flake/configuration. + +## Installing libraries versus installing executables + +In case you did not know already, +you can't install libraries globally on Nix as you would with regular +package managers like `pacman`, `dnf` or `apt`. You have to write a +derivation for your projects like you would for any other program. +If you try to install a library through `home.packages` or `environment.systemPackages` +don't expect it to be picked up from runtimes. + +However, if you want to use the CLI tool that comes with some of the libraries +you have to **also** install them through `home.packages` or `environment.systemPackages` +alongside your derivations. + +### Astal CLI + +The core library also comes with a CLI tool that you can use to send +requests to your app. + +:::code-group + +```nix [nixos] +environment.systemPackages = [inputs.astal.packages.${system}.default]; +``` + +```nix [home-manager] +home.packages = [inputs.astal.packages.${system}.default]; +``` + +::: + +```sh [astal cli] +astal --list # list running instances +``` ## TypeScript diff --git a/docs/guide/getting-started/supported-languages.md b/docs/guide/getting-started/supported-languages.md index eacd1da..c38a001 100644 --- a/docs/guide/getting-started/supported-languages.md +++ b/docs/guide/getting-started/supported-languages.md @@ -18,16 +18,16 @@ The runtime is [GJS](https://gitlab.gnome.org/GNOME/gjs) and **not** nodejs Examples: -- [Simple Bar](https://github.com/Aylur/astal/tree/main/examples/js/simple-bar) +- [Simple Bar](https://github.com/Aylur/astal/tree/main/examples/gtk3/js/simple-bar)  -- [Notification Popups](https://github.com/Aylur/astal/tree/main/examples/js/notifications) +- [Notification Popups](https://github.com/Aylur/astal/tree/main/examples/gtk3/js/notifications)  -- [Applauncher](https://github.com/Aylur/astal/tree/main/examples/js/applauncher) +- [Applauncher](https://github.com/Aylur/astal/tree/main/examples/gtk3/js/applauncher)  -- [Media Player](https://github.com/Aylur/astal/tree/main/examples/js/media-player) +- [Media Player](https://github.com/Aylur/astal/tree/main/examples/gtk3/js/media-player)  ## Lua @@ -36,9 +36,18 @@ Lua is well-supported, but I would still recommend TypeScript, as Lua lacks a ty Examples: -- [Simple Bar](https://github.com/Aylur/astal/tree/main/examples/lua/simple-bar) +- [Simple Bar](https://github.com/Aylur/astal/tree/main/examples/gtk3/lua/simple-bar)  +- [Notification Popups](https://github.com/Aylur/astal/tree/main/examples/lua/notifications) + + +- [Applauncher](https://github.com/Aylur/astal/tree/main/examples/lua/applauncher) + + +- [Media Player](https://github.com/Aylur/astal/tree/main/examples/lua/media-player) + + ## Python There is a WIP [package for python](https://github.com/aylur/astal/tree/feat/python), @@ -47,7 +56,7 @@ However, you can still use python the OOP way [pygobject](https://pygobject.gnom Examples: -- [Simple Bar](https://github.com/Aylur/astal/tree/main/examples/py/simple-bar) +- [Simple Bar](https://github.com/Aylur/astal/tree/main/examples/gtk3/py/simple-bar)  ## Vala @@ -58,7 +67,7 @@ using TypeScript or Lua over Vala as they are simpler to work with. Examples: -- [Simple Bar](https://github.com/Aylur/astal/tree/main/examples/vala/simple-bar) +- [Simple Bar](https://github.com/Aylur/astal/tree/main/examples/gtk3/vala/simple-bar)  ## C diff --git a/docs/guide/libraries/apps.md b/docs/guide/libraries/apps.md index f1748db..4dba9ba 100644 --- a/docs/guide/libraries/apps.md +++ b/docs/guide/libraries/apps.md @@ -80,7 +80,7 @@ foreach (var app in apps.fuzzy_query("firefox")) { :::code-group ```sh [<i class="devicon-archlinux-plain"></i> Arch] -sudo pacman -Syu meson vala json-glib gobject-introspection +sudo pacman -Syu meson vala valadoc json-glib gobject-introspection ``` ```sh [<i class="devicon-fedora-plain"></i> Fedora] @@ -88,7 +88,7 @@ sudo dnf install meson vala valadoc json-glib-devel gobject-introspection-devel ``` ```sh [<i class="devicon-ubuntu-plain"></i> Ubuntu] -sudo apt install meson valac libjson-glib-dev gobject-introspection +sudo apt install meson valac valadoc libjson-glib-dev gobject-introspection ``` ::: diff --git a/docs/guide/libraries/battery.md b/docs/guide/libraries/battery.md index 7e6fe24..0a06532 100644 --- a/docs/guide/libraries/battery.md +++ b/docs/guide/libraries/battery.md @@ -53,7 +53,7 @@ print(battery.percentage) :::code-group ```sh [<i class="devicon-archlinux-plain"></i> Arch] -sudo pacman -Syu meson vala json-glib gobject-introspection +sudo pacman -Syu meson vala valadoc json-glib gobject-introspection ``` ```sh [<i class="devicon-fedora-plain"></i> Fedora] @@ -61,7 +61,7 @@ sudo dnf install meson vala valadoc json-glib-devel gobject-introspection-devel ``` ```sh [<i class="devicon-ubuntu-plain"></i> Ubuntu] -sudo apt install meson valac libjson-glib-dev gobject-introspection +sudo apt install meson valac valadoc libjson-glib-dev gobject-introspection ``` ::: diff --git a/docs/guide/libraries/bluetooth.md b/docs/guide/libraries/bluetooth.md index 9a3e5b8..e1e7508 100644 --- a/docs/guide/libraries/bluetooth.md +++ b/docs/guide/libraries/bluetooth.md @@ -60,7 +60,7 @@ end :::code-group ```sh [<i class="devicon-archlinux-plain"></i> Arch] -sudo pacman -Syu meson vala gobject-introspection +sudo pacman -Syu meson vala valadoc gobject-introspection ``` ```sh [<i class="devicon-fedora-plain"></i> Fedora] @@ -68,7 +68,7 @@ sudo dnf install meson vala valadoc gobject-introspection-devel ``` ```sh [<i class="devicon-ubuntu-plain"></i> Ubuntu] -sudo apt install meson valac gobject-introspection +sudo apt install meson valac valadoc gobject-introspection ``` ::: diff --git a/docs/guide/libraries/greet.md b/docs/guide/libraries/greet.md index 47f98b9..9c2f1b7 100644 --- a/docs/guide/libraries/greet.md +++ b/docs/guide/libraries/greet.md @@ -61,7 +61,7 @@ try { :::code-group ```sh [<i class="devicon-archlinux-plain"></i> Arch] -sudo pacman -Syu meson vala json-glib gobject-introspection +sudo pacman -Syu meson vala valadoc json-glib gobject-introspection ``` ```sh [<i class="devicon-fedora-plain"></i> Fedora] @@ -69,7 +69,7 @@ sudo dnf install meson vala valadoc json-glib-devel gobject-introspection-devel ``` ```sh [<i class="devicon-ubuntu-plain"></i> Ubuntu] -sudo apt install meson valac libjson-glib-dev gobject-introspection +sudo apt install meson valac valadoc libjson-glib-dev gobject-introspection ``` ::: diff --git a/docs/guide/libraries/hyprland.md b/docs/guide/libraries/hyprland.md index 82d9e9d..5eefa2e 100644 --- a/docs/guide/libraries/hyprland.md +++ b/docs/guide/libraries/hyprland.md @@ -58,7 +58,7 @@ end :::code-group ```sh [<i class="devicon-archlinux-plain"></i> Arch] -sudo pacman -Syu meson vala json-glib gobject-introspection +sudo pacman -Syu meson vala valadoc json-glib gobject-introspection ``` ```sh [<i class="devicon-fedora-plain"></i> Fedora] @@ -66,7 +66,7 @@ sudo dnf install meson vala valadoc json-glib-devel gobject-introspection-devel ``` ```sh [<i class="devicon-ubuntu-plain"></i> Ubuntu] -sudo apt install meson valac libjson-glib-dev gobject-introspection +sudo apt install meson valac valadoc libjson-glib-dev gobject-introspection ``` ::: diff --git a/docs/guide/libraries/mpris.md b/docs/guide/libraries/mpris.md index c2283cc..768bb45 100644 --- a/docs/guide/libraries/mpris.md +++ b/docs/guide/libraries/mpris.md @@ -61,7 +61,7 @@ end :::code-group ```sh [<i class="devicon-archlinux-plain"></i> Arch] -sudo pacman -Syu meson vala gvfs json-glib gobject-introspection +sudo pacman -Syu meson vala valadoc gvfs json-glib gobject-introspection ``` ```sh [<i class="devicon-fedora-plain"></i> Fedora] @@ -69,7 +69,7 @@ sudo dnf install meson vala valadoc gvfs json-glib-devel gobject-introspection-d ``` ```sh [<i class="devicon-ubuntu-plain"></i> Ubuntu] -sudo apt install meson valac gvfs libjson-glib-dev gobject-introspection +sudo apt install meson valac valadoc gvfs libjson-glib-dev gobject-introspection ``` ::: diff --git a/docs/guide/libraries/network.md b/docs/guide/libraries/network.md index 79a217c..0d9d6a8 100644 --- a/docs/guide/libraries/network.md +++ b/docs/guide/libraries/network.md @@ -55,7 +55,7 @@ print(network.wifi.ssid) :::code-group ```sh [<i class="devicon-archlinux-plain"></i> Arch] -sudo pacman -Syu meson vala libnm gobject-introspection +sudo pacman -Syu meson vala valadoc libnm gobject-introspection ``` ```sh [<i class="devicon-fedora-plain"></i> Fedora] @@ -63,7 +63,7 @@ sudo dnf install meson vala valadoc NetworkManager-libnm-devel gobject-introspec ``` ```sh [<i class="devicon-ubuntu-plain"></i> Ubuntu] -sudo apt install meson valac libnm-dev gobject-introspection +sudo apt install meson valac valadoc libnm-dev gobject-introspection ``` ::: diff --git a/docs/guide/libraries/notifd.md b/docs/guide/libraries/notifd.md index 1d61099..4112f94 100644 --- a/docs/guide/libraries/notifd.md +++ b/docs/guide/libraries/notifd.md @@ -67,7 +67,7 @@ end :::code-group ```sh [<i class="devicon-archlinux-plain"></i> Arch] -sudo pacman -Syu meson vala gdk-pixbuf2 json-glib gobject-introspection +sudo pacman -Syu meson vala valadoc gdk-pixbuf2 json-glib gobject-introspection ``` ```sh [<i class="devicon-fedora-plain"></i> Fedora] @@ -75,7 +75,7 @@ sudo dnf install meson vala valadoc gdk-pixbuf2-devel json-glib-devel gobject-in ``` ```sh [<i class="devicon-ubuntu-plain"></i> Ubuntu] -sudo apt install meson valac libgdk-pixbuf-2.0-dev libjson-glib-dev gobject-introspection +sudo apt install meson valac valadoc libgdk-pixbuf-2.0-dev libjson-glib-dev gobject-introspection ``` ::: diff --git a/docs/guide/libraries/powerprofiles.md b/docs/guide/libraries/powerprofiles.md index b42d7c6..a9e7559 100644 --- a/docs/guide/libraries/powerprofiles.md +++ b/docs/guide/libraries/powerprofiles.md @@ -53,7 +53,7 @@ print(powerprofiles.active_profile) :::code-group ```sh [<i class="devicon-archlinux-plain"></i> Arch] -sudo pacman -Syu meson vala json-glib gobject-introspection +sudo pacman -Syu meson vala valadoc json-glib gobject-introspection ``` ```sh [<i class="devicon-fedora-plain"></i> Fedora] @@ -61,7 +61,7 @@ sudo dnf install meson vala valadoc json-glib-devel gobject-introspection-devel ``` ```sh [<i class="devicon-ubuntu-plain"></i> Ubuntu] -sudo apt install meson valac libjson-glib-dev gobject-introspection +sudo apt install meson valac valadoc libjson-glib-dev gobject-introspection ``` ::: diff --git a/docs/guide/libraries/tray.md b/docs/guide/libraries/tray.md index 43b3aa6..a7d4c33 100644 --- a/docs/guide/libraries/tray.md +++ b/docs/guide/libraries/tray.md @@ -58,27 +58,36 @@ end :::code-group ```sh [<i class="devicon-archlinux-plain"></i> Arch] -sudo pacman -Syu meson gtk3 gobject-introspection libdbusmenu-gtk3 +sudo pacman -Syu meson json-glib gobject-introspection ``` ```sh [<i class="devicon-fedora-plain"></i> Fedora] -sudo dnf install meson gcc gtk3-devel libdbusmenu-gtk3-devel gobject-introspection-devel +sudo dnf install meson json-glib-devel gobject-introspection-devel ``` ```sh [<i class="devicon-ubuntu-plain"></i> Ubuntu] -sudo apt install meson libgtk-3-dev libdbusmenu-gtk3-dev gobject-introspection +sudo apt install meson libjson-glib-dev gobject-introspection ``` ::: -2. clone repo +2. install `appmenu-glib-translator` + +```sh +git clone https://github.com/rilian-la-te/vala-panel-appmenu.git +cd vala-panel-appmenu/subprojects/appmenu-glib-translator +meson setup --prefix /usr build +meson install -C build +``` + +3. clone repo ```sh git clone https://github.com/aylur/astal.git cd astal/lib/tray ``` -3. install +4. install ```sh meson setup --prefix /usr build diff --git a/docs/guide/libraries/wireplumber.md b/docs/guide/libraries/wireplumber.md index d6faea1..b3dbf6d 100644 --- a/docs/guide/libraries/wireplumber.md +++ b/docs/guide/libraries/wireplumber.md @@ -55,7 +55,7 @@ print(audio.default_speaker.volume) :::code-group ```sh [<i class="devicon-archlinux-plain"></i> Arch] -sudo pacman -Syu meson vala wireplumber gobject-introspection +sudo pacman -Syu meson vala valadoc wireplumber gobject-introspection ``` ```sh [<i class="devicon-fedora-plain"></i> Fedora] diff --git a/docs/guide/lua/binding.md b/docs/guide/lua/binding.md index f4d5f0b..f9957b5 100644 --- a/docs/guide/lua/binding.md +++ b/docs/guide/lua/binding.md @@ -68,12 +68,13 @@ local Gtk = require("astal.gtk3").Gtk local Variable = require("astal.variable") ---@param initial table +---@return varmap return function(initial) local map = initial - local var = Variable() + local var = Variable.new({}) local function notify() - local arr + local arr = {} for _, value in pairs(map) do table.insert(arr, value) end @@ -90,7 +91,13 @@ return function(initial) notify() -- init - return { + ---@class varmap + ---@field set fun(key: any, value: any): nil + ---@field delete fun(key: any): nil + ---@field get fun(): any + ---@field subscribe fun(callback: function): function + ---@overload fun(): Binding + return setmetatable({ set = function(key, value) delete(key) map[key] = value @@ -106,7 +113,11 @@ return function(initial) subscribe = function(callback) return var:subscribe(callback) end, - } + }, { + __call = function() + return var() + end, + }) end ``` @@ -130,7 +141,7 @@ function MappedBox() map.delete(id) end) end, - bind(map):as(function (arr) + map():as(function(arr) -- can be sorted here return arr end), diff --git a/docs/guide/lua/examples.md b/docs/guide/lua/examples.md index be46b6e..464f917 100644 --- a/docs/guide/lua/examples.md +++ b/docs/guide/lua/examples.md @@ -1,4 +1,13 @@ # Lua examples -## [Simple Bar](https://github.com/Aylur/astal/tree/main/examples/lua/simple-bar) +## [Simple Bar](https://github.com/Aylur/astal/tree/main/examples/gtk3/lua/simple-bar)  + +### [Notification Popups](https://github.com/Aylur/astal/tree/main/examples/gtk3/lua/notifications) + + +### [Applauncher](https://github.com/Aylur/astal/tree/main/examples/gtk3/lua/applauncher) + + +### [Media Player](https://github.com/Aylur/astal/tree/main/examples/gtk3/lua/media-player) + diff --git a/docs/guide/typescript/cli-app.md b/docs/guide/typescript/cli-app.md index 9b299aa..41b1d7c 100644 --- a/docs/guide/typescript/cli-app.md +++ b/docs/guide/typescript/cli-app.md @@ -26,7 +26,7 @@ App.start({ ## Instance identifier -You can run multiple instance by defining a unique instance name. +You can run multiple instances by defining a unique instance name. ```ts App.start({ @@ -44,7 +44,7 @@ you can do so by sending a message. App.start({ requestHandler(request: string, res: (response: any) => void) { if (request == "say hi") { - res("hi cli") + return res("hi cli") } res("unknown command") }, @@ -140,7 +140,7 @@ App.start({ // every subsequent calls client(message: (msg: string) => string, ...args: Array<string>) { const res = message("you can message the main instance") - console.log(res) + print(res) }, // this runs in the main instance diff --git a/docs/guide/typescript/examples.md b/docs/guide/typescript/examples.md index ec51e89..81a6f35 100644 --- a/docs/guide/typescript/examples.md +++ b/docs/guide/typescript/examples.md @@ -2,13 +2,16 @@ ## Gtk3 -### [Simple Bar](https://github.com/Aylur/astal/tree/main/examples/js/simple-bar) +### [Simple Bar](https://github.com/Aylur/astal/tree/main/examples/gtk3/js/simple-bar)  -### [Notification Popups](https://github.com/Aylur/astal/tree/main/examples/js/notifications) +### [Notification Popups](https://github.com/Aylur/astal/tree/main/examples/gtk3/js/notifications)  -### [Applauncher](https://github.com/Aylur/astal/tree/main/examples/js/applauncher) +### [Applauncher](https://github.com/Aylur/astal/tree/main/examples/gtk3/js/applauncher)  -### [Media Player](https://github.com/Aylur/astal/tree/main/examples/js/media-player) +### [Media Player](https://github.com/Aylur/astal/tree/main/examples/gtk3/js/media-player)  + +### [OSD](https://github.com/Aylur/astal/tree/main/examples/gtk3/js/osd) + diff --git a/docs/guide/typescript/faq.md b/docs/guide/typescript/faq.md index 4ee616b..15a97e5 100644 --- a/docs/guide/typescript/faq.md +++ b/docs/guide/typescript/faq.md @@ -55,14 +55,14 @@ const HOME = GLib.getenv("HOME") ## Custom SVG symbolic icons -Put the svgs in a directory, named `<icon-name>-symbolic.svg` +Put the svgs in a directory, name them `<icon-name>-symbolic.svg` and use `App.add_icons` or `icons` parameter in `App.start` :::code-group ```ts [app.ts] App.start({ - icons: `${SRC}/icons`, + icons: `/path/to/icons`, // this dir should include custom-symbolic.svg main() { Widget.Icon({ icon: "custom-symbolic", // custom-symbolic.svg @@ -269,3 +269,41 @@ class MyWidget extends Widget.Box { } } ``` + +## How do I register keybindings? + +If you want global keybindings use your compositor. +Only **focused** windows can capture events. To make a window +focusable set its keymode. + +::: code-group +```tsx [gtk3] +<window + keymode={Astal.Keymode.ON_DEMAND} + onKeyPressEvent={(self, event: Gdk.Event) => { + if (event.get_keyval()[1] === Gdk.KEY_Escape) { + self.hide() + } + }} +/> +``` + +```tsx [gtk4] +<window + keymode={Astal.Keymode.ON_DEMAND} + onKeyPressed={(self, keyval) => { + if (keyval === Gdk.KEY_Escape) { + self.hide() + } + }} +/> +``` +::: + +## How to create a Popup + +In Gtk4 simply use Gtk's builtin [Popover](https://docs.gtk.org/gtk4/class.Popover.html). + +In Gtk3 you can create an [Astal.Window](https://aylur.github.io/libastal/astal3/class.Window.html) and handle click events. + +Checkout [examples/gtk3/js/popover](https://github.com/Aylur/astal/tree/main/examples/gtk3/js/popover) diff --git a/docs/guide/typescript/first-widgets.md b/docs/guide/typescript/first-widgets.md index 77b2f61..9b8bf32 100644 --- a/docs/guide/typescript/first-widgets.md +++ b/docs/guide/typescript/first-widgets.md @@ -71,7 +71,7 @@ function MyButton(): JSX.Element { } ``` -```ts [MyButton.ts] +```ts [MyButton.ts (gtk3)] import { Widget } from "astal/gtk3" function MyButton(): Widget.Button { @@ -82,6 +82,17 @@ function MyButton(): Widget.Button { } ``` +```ts [MyButton.ts (gtk4)] +import { Widget } from "astal/gtk4" + +function MyButton(): Widget.Button { + return Widget.Button( + { onClicked: "echo hello" }, + Widget.Label({ label: "Click me!" }), + ) +} +``` + ::: :::info @@ -218,6 +229,14 @@ Their types are not generated, but written by hand, which means not all of them Refer to the Gtk and Astal docs to have a full list of them. ::: +:::info +Attributes prefixed with `onNotify` will connect to a `notify::` signal of the widget. + +```tsx +<switch onNotifyActive={self => print("switched to", self.active)}> +``` +::: + ## How properties are passed Using JSX, a custom widget will always have a single object as its parameter. @@ -413,8 +432,7 @@ function Parent(props: { :::tip If you have a widget where you pass widgets in various ways, you can -wrap `child` in `children` in a [`Subscribable`](./faq#custom-widgets-with-bindable-properties) and handle all cases -as if they were bindings. +wrap `child` and `children` props in a [`Subscribable`](./faq#custom-widgets-with-bindable-properties) and handle all cases as if they were bindings. ::: :::info diff --git a/docs/guide/typescript/theming.md b/docs/guide/typescript/theming.md index 5944c4e..4cb3486 100644 --- a/docs/guide/typescript/theming.md +++ b/docs/guide/typescript/theming.md @@ -1,24 +1,25 @@ # Theming -Since the widget toolkit is **GTK3** theming is done with **CSS**. +Since the widget toolkit is **GTK** theming is done with **CSS**. - [CSS tutorial](https://www.w3schools.com/css/) -- [GTK CSS Overview wiki](https://docs.gtk.org/gtk3/css-overview.html) -- [GTK CSS Properties Overview wiki](https://docs.gtk.org/gtk3/css-properties.html) +- [GTK3 CSS Overview wiki](https://docs.gtk.org/gtk3/css-overview.html) +- [GTK3 CSS Properties Overview wiki](https://docs.gtk.org/gtk3/css-properties.html) +- [GTK4 CSS Overview wiki](https://docs.gtk.org/gtk4/css-overview.html) +- [GTK4 CSS Properties Overview wiki](https://docs.gtk.org/gtk4/css-properties.html) :::warning GTK is not the web While most features are implemented in GTK, you can't assume anything that works on the web will work with GTK. -Refer to the [GTK docs](https://docs.gtk.org/gtk3/css-overview.html) -to see what is available. +Refer to the GTK docs to see what is available. ::: -So far every widget you made used your default GTK3 theme. +So far every widget you made used your default GTK theme. To make them more custom, you can apply stylesheets to them. ## From file at startup -You can pass a path to a file or css as a string in `App.start` +You can pass a path to a file or CSS as a string in `App.start` :::code-group @@ -118,7 +119,7 @@ npm install -g sass # not packaged on Ubuntu ```ts [app.ts] import { exec } from "astal/process" -exec("sass", "./style.scss", "/tmp/style.css") +exec("sass ./style.scss /tmp/style.css") App.start({ css: "/tmp/style.css", diff --git a/docs/guide/typescript/widget.md b/docs/guide/typescript/widget.md index 7ed69e3..86e666b 100644 --- a/docs/guide/typescript/widget.md +++ b/docs/guide/typescript/widget.md @@ -6,15 +6,21 @@ These are properties that Astal additionally adds to Gtk.Widgets -- className: `string` - List of class CSS selectors separated by white space. -- css: `string` - Inline CSS. e.g `label { color: white; }`. If no selector is specified `*` will be assumed. e.g `color: white;` will be inferred as `* { color: white; }`. -- cursor: `string` - Cursor style when hovering over widgets that have hover states, e.g it won't work on labels. [list of valid values](https://docs.gtk.org/gdk3/ctor.Cursor.new_from_name.html). -- clickThrough: `boolean` - Lets click events through. +- `className`: `string` - List of class CSS selectors separated by white space. +- `css`: `string` - Inline CSS. e.g `label { color: white; }`. If no selector is specified `*` will be assumed. e.g `color: white;` will be inferred as `* { color: white; }`. +- `cursor`: `string` - Cursor style when hovering over widgets that have hover states, e.g it won't work on labels. [list of valid values](https://docs.gtk.org/gdk3/ctor.Cursor.new_from_name.html). +- `clickThrough`: `boolean` - Lets click events through. To have a full list of available properties, reference the documentation of the widget. - [Astal3 widgets](https://aylur.github.io/libastal/astal3/index.html#classes) -- [Gtk widgets](https://docs.gtk.org/gtk3/#classes) +- [Gtk3 widgets](https://docs.gtk.org/gtk3/#classes) + +Most common ones you will use frequently are + - [halign](https://docs.gtk.org/gtk3/property.Widget.halign.html) + - [valign](https://docs.gtk.org/gtk3/property.Widget.valign.html) + - [hexpand](https://docs.gtk.org/gtk3/property.Widget.hexpand.html) + - [vexpand](https://docs.gtk.org/gtk3/property.Widget.vexpand.html) ### Additional widget methods @@ -27,7 +33,7 @@ without `setup` ```tsx function MyWidget() { - const button = Widget.Button() + const button = new Widget.Button() // setup button return button } @@ -94,14 +100,14 @@ function MyWidget() { ### How to use non builtin Gtk widgets -Using the `Widget.astalify` mixin you can subclass widgets +Using the `astalify` mixin you can subclass widgets to behave like builtin widgets. The `astalify` mixin will apply the following: - set `visible` to true by default (Gtk3 widgets are invisible by default) - make gobject properties accept and consume `Binding` objects - add properties and methods listed above -- sets up signal handlers that are passed as props prefixed with `on` +- set up signal handlers that are passed as props prefixed with `on` ```tsx import GObject from "gi://GObject" @@ -135,7 +141,7 @@ function MyWidget() { alpha: 0.5, })} onColorSet={(self) => { - console.log(self.rgba) + print(self.rgba) }} /> } @@ -144,7 +150,7 @@ function MyWidget() { :::info Signal properties have to be annotated manually for TypeScript. You can reference [Gtk3](https://gjs-docs.gnome.org/gtk30~3.0/) -and [Astal](https://aylur.github.io/libastal/index.html#classes) for available signals. +and [Astal3](https://aylur.github.io/libastal/astal3/#classes) for available signals. ::: ### TypeScript @@ -189,28 +195,377 @@ export default function ToggleButton(btnprops: ToggleButtonProps) { ### Builtin Widgets -You can check the [source code](https://github.com/aylur/astal/blob/main/lang/gjs/src/gtk3/index.ts) to have a full list of builtin widgets. - These widgets are available by default in JSX. - box: [Astal.Box](https://aylur.github.io/libastal/astal3/class.Box.html) + ```tsx + <box>Horizontal Box</box> + ``` + ```tsx + <box orientation={1}>Vertical Box</box> + ``` - button: [Astal.Button](https://aylur.github.io/libastal/astal3/class.Button.html) + ```tsx + <button onClicked={self => print(self, "was clicked")}> + Click Me + </button> + ``` - centerbox: [Astal.CenterBox](https://aylur.github.io/libastal/astal3/class.CenterBox.html) + ```tsx + <centerbox orientation={1}> + <label vexpand valign={Gtk.Align.START} label="Start Widget" /> + <label label="Center Widget" /> + <label vexpand valign={Gtk.Align.END} label="End Widget" /> + </box> + ``` - circularprogress: [Astal.CircularProgress](https://aylur.github.io/libastal/astal3/class.CircularProgress.html) + ```tsx + <circularprogress value={.5} startAt={0.75} endAt={0.75}> + <icon /> + </circularprogress> + ``` + ```css + circularprogress { + color: green; + background-color: black; + font-size: 6px; + margin: 2px; + min-width: 32px; + } + ``` + - drawingarea: [Gtk.DrawingArea](https://docs.gtk.org/gtk3/class.DrawingArea.html) + ```tsx + <drawingarea onDraw={drawingFunction} /> + ``` + - entry: [Gtk.Entry](https://docs.gtk.org/gtk3/class.Entry.html) + ```tsx + <window keymode={Astal.Keymode.ON_DEMAND}> + <entry + onChanged={self => print("text changed", self.text)} + onActivate={self => print("enter", self.text)} + /> + </window> + ``` + - eventbox: [Astal.EventBox](https://aylur.github.io/libastal/astal3/class.EventBox.html) + ```tsx + <eventbox + onClick={(_, event) => { + print(event.modifier, event.button) + }} + /> + ``` + - icon: [Astal.Icon](https://aylur.github.io/libastal/astal3/class.Icon.html) + ```tsx + <icon icon={GLib.get_os_info("LOGO") || "missing-symbolic"} /> + ``` + ```css + icon { + font-size: 16px; + } + ``` + - label: [Astal.Label](https://aylur.github.io/libastal/astal3/class.Label.html) + ```tsx + <label label="hello" maxWidthChars={16} wrap /> + ``` + - levelbar: [Astal.LevelBar](https://aylur.github.io/libastal/astal3/class.LevelBar.html) + ```tsx + <levelbar value={0.5} widthRequest={200} /> + ``` + - overlay: [Astal.Overlay](https://aylur.github.io/libastal/astal3/class.Overlay.html) + ```tsx + <overlay> + <box heightRequest={40} widthRequest={40}>Child</box> + <box className="overlay" valign={Gtk.Align.START} halign={Gtk.Align.END}>1</box> + </overlay> + ``` + - revealer: [Gtk.Revealer](https://docs.gtk.org/gtk3/class.Revealer.html) + ```tsx + <revealer + setup={self => timeout(500, () => self.revealChild = true)} + transitionType={Gtk.RevealerTransitionType.SLIDE_UP}> + <label label="Child" /> + </revealer> + ``` + - scrollable: [Astal.Scrollable](https://aylur.github.io/libastal/astal3/class.Scrollable.html) + ```tsx + <scrollable heightRequest={100}> + <box orientation={1}> + {Array.from({ length: 10 }, (_, i) => ( + <button>{i}</button> + ))} + </box> + </scrollable> + ``` + - slider: [Astal.Slider](https://aylur.github.io/libastal/astal3/class.Slider.html) + ```tsx + <slider widthRequest={100} onDragged={self => print("new value", self.value)} /> + ``` + - stack: [Astal.Stack](https://aylur.github.io/libastal/astal3/class.Stack.html) + ```tsx + <stack visibleChildName="child2"> + <label name="child1" label="child1" /> + <label name="child2" label="child2" /> + </stack> + ``` + - switch: [Gtk.Switch](https://docs.gtk.org/gtk3/class.Switch.html) + ```tsx + <switch onNotifyActive={self => print(self.active)} /> + ``` + - window: [Astal.Window](https://aylur.github.io/libastal/astal3/class.Window.html) + ```tsx + <window + className="Bar" + name="bar" + namespace="bar" + application={App} + monitor={0} + anchor={Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT} + exclusivity={Astal.Exclusivity.EXCLUSIVE} + keymode={Astal.Keymode.ON_DEMAND} + > + <centerbox /> + </window> + ``` ## Gtk4 -🚧 Work in Progress 🚧 +The Gtk4 js library does not add any additional properties to the widgets, +but it still has some additional properties that the constructors handle. + +- `type`: `string` an arbitrary string that the [Buildable](https://docs.gtk.org/gtk4/iface.Buildable.html) interface uses. +- event handlers for [EventControllers](https://docs.gtk.org/gtk4/class.EventController.html) + ```ts + type EventController<Self extends Gtk.Widget> = { + onFocusEnter?: (self: Self) => void + onFocusLeave?: (self: Self) => void + + onKeyPressed?: (self: Self, keyval: number, keycode: number, state: Gdk.ModifierType) => void + onKeyReleased?: (self: Self, keyval: number, keycode: number, state: Gdk.ModifierType) => void + onKeyModifier?: (self: Self, state: Gdk.ModifierType) => void + + onLegacy?: (self: Self, event: Gdk.Event) => void + onButtonPressed?: (self: Self, state: Gdk.ButtonEvent) => void + onButtonReleased?: (self: Self, state: Gdk.ButtonEvent) => void + + onHoverEnter?: (self: Self, x: number, y: number) => void + onHoverLeave?: (self: Self) => void + onMotion?: (self: Self, x: number, y: number) => void + + onScroll?: (self: Self, dx: number, dy: number) => void + onScrollDecelerate?: (self: Self, vel_x: number, vel_y: number) => void + } + ``` + +- `setup`: `(self): void` setup function that runs after constructor + ```tsx + // without `setup` + function MyWidget() { + const button = Widget.Button() + // setup button + return button + } + + // using `setup` + function MyWidget() { + function setup(button: Widget.Button) { + // setup button + } + + return <buttons setup={setup} /> + } + ``` + +There is also a `hook` utility + +```tsx +// without `hook` +function MyWidget() { + const id = gobject.connect("signal", callback) + const unsub = variable.subscribe(callback) + + return <box + onDestroy={() => { + gobject.disconnect(id) + unsub() + }} + /> +} + +// with `hook` +import { hook } from "astal/gtk4" + +function MyWidget() { + return <box + setup={(self) => { + self.hook(gobject, "signal", callback) + self.hook(variable, callback) + }} + /> +} +``` + +### How to use non builtin Gtk widgets + +Using the `astalify` function you can create wrappers around widget constructors +to make them behave like builtin widgets. +The `astalify` function will do the followings: + +- make `gobject` properties accept and consume `Binding` objects +- handle properties listed above +- set up signal handlers that are passed as props prefixed with `on` + +```tsx +import GObject from "gi://GObject" +import { Gtk, astalify, type ConstructProps } from "astal/gtk4" + +type CalendarProps = ConstructProps<Gtk.Calendar, Gtk.Calendar.ConstructorProps> +const Calendar = astalify<Gtk.Calendar, Gtk.Calendar.ConstructorProps>(Gtk.Calendar, { + // if it is a container widget, define children setter and getter here + getChildren(self) { return [] }, + setChildren(self, children) {}, +}) + +function MyWidget() { + function setup(button: Gtk.Calendar) { + + } + + return <Calendar + setup={setup} + onDaySelected={(self) => { + print(self.day) + }} + /> +} +``` + +### Builtin Widgets + +These widgets are available by default in JSX. + +- box: [Astal.Box](https://aylur.github.io/libastal/astal4/class.Box.html) + ```tsx + <box>Horizontal Box</box> + ``` + ```tsx + <box orientation={1}>Vertical Box</box> + ``` +- button: [Gtk.Button](https://docs.gtk.org/gtk4/class.Button.html) + ```tsx + <button onClicked={self => print(self, "was clicked")}> + Click Me + </button> + ``` +- centerbox: [Gtk.CenterBox](https://docs.gtk.org/gtk4/class.CenterBox.html) + ```tsx + <centerbox orientation={1}> + <label label="Start Widget" /> + <label label="Center Widget" /> + <label label="End Widget" /> + </box> + ``` +- entry: [Gtk.Entry](https://docs.gtk.org/gtk4/class.Entry.html) + ```tsx + <window keymode={Astal.Keymode.ON_DEMAND}> + <entry + onNotifyText={self => print("text changed", self.text)} + onActivate={self => print("enter", self.text)} + /> + </window> + ``` + +- image: [Gtk.Image](https://docs.gtk.org/gtk4/class.Image.html) + ```tsx + <image iconName={GLib.get_os_info("LOGO") || "missing-symbolic"} /> + ``` + ```css + image { + -gtk-icon-size: 16px; + } + ``` + +- label: [Gtk.Label](https://docs.gtk.org/gtk4/class.Label.html) + ```tsx + <label label="hello" maxWidthChars={16} wrap /> + ``` + +- levelbar: [Gtk.LevelBar](https://docs.gtk.org/gtk4/class.LevelBar.html) + ```tsx + <levelbar value={0.5} widthRequest={200} /> + ``` + +- overlay: [Gtk.Overlay](https://docs.gtk.org/gtk4/class.Overlay.html) + ```tsx + <overlay> + <box heightRequest={40} widthRequest={40}>Child</box> + <box type="overlay measure" >1</box> + <box type="overlay clip" >2</box> + <box type="overlay clip measure" >3</box> + </overlay> + ``` + +- revealer: [Gtk.Revealer](https://docs.gtk.org/gtk4/class.Revealer.html) + ```tsx + <revealer + setup={self => timeout(500, () => self.revealChild = true)} + transitionType={Gtk.RevealerTransitionType.SLIDE_UP}> + <label label="Child" /> + </revealer> + ``` + +- slider: [Astal.Slider](https://aylur.github.io/libastal/astal4/class.Slider.html) + ```tsx + <slider widthRequest={100} onNotifyValue={self => print("new value", self.value)} /> + ``` + +- stack: [Gtk.Stack](https://docs.gtk.org/gtk4/class.Stack.html) + ```tsx + <stack visibleChildName="child2"> + <label name="child1" label="child1" /> + <label name="child2" label="child2" /> + </stack> + ``` + +- switch: [Gtk.Switch](https://docs.gtk.org/gtk4/class.Switch.html) + ```tsx + <switch onNotifyActive={self => print(self.active)} /> + ``` + +- menubutton: [Gtk.MenuButton](https://docs.gtk.org/gtk4/class.MenuButton.html) and popover: [Gtk.Popover](https://docs.gtk.org/gtk4/class.Popover.html) + ```tsx + <menubutton> + <label label="Button Content" /> + <popover> + <label label="Popover Content" /> + </popover> + </menubutton> + ``` + +- window: [Astal.Window](https://aylur.github.io/libastal/astal4/class.Window.html) + ```tsx + <window + cssClasses={["Bar"]} + name="bar" + namespace="bar" + application={App} + monitor={0} + anchor={Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT} + exclusivity={Astal.Exclusivity.EXCLUSIVE} + keymode={Astal.Keymode.ON_DEMAND} + > + <centerbox /> + </window> + ``` diff --git a/docs/package.json b/docs/package.json index ad76c9a..e01b189 100644 --- a/docs/package.json +++ b/docs/package.json @@ -12,7 +12,7 @@ "vue": "^3.4.38" }, "scripts": { - "dev": "vitepress dev", + "dev": "vitepress dev --open", "build": "vitepress build", "preview": "vitepress preview", "vitepress": "vitepress", diff --git a/docs/public/showcase/aylur.png b/docs/public/showcase/aylur.png Binary files differdeleted file mode 100644 index 5dc5dd7..0000000 --- a/docs/public/showcase/aylur.png +++ /dev/null diff --git a/docs/public/showcase/aylur.webp b/docs/public/showcase/aylur.webp Binary files differnew file mode 100644 index 0000000..7f89e04 --- /dev/null +++ b/docs/public/showcase/aylur.webp diff --git a/docs/public/showcase/ezerinz.webp b/docs/public/showcase/ezerinz.webp Binary files differnew file mode 100644 index 0000000..7da8d7d --- /dev/null +++ b/docs/public/showcase/ezerinz.webp diff --git a/docs/public/showcase/hyprpanel_showcase.webp b/docs/public/showcase/hyprpanel_showcase.webp Binary files differnew file mode 100644 index 0000000..6a9ad7c --- /dev/null +++ b/docs/public/showcase/hyprpanel_showcase.webp diff --git a/docs/public/showcase/tokyob0t-super-duper-hiper-mega-ultra-contribution.webp b/docs/public/showcase/tokyob0t.webp Binary files differindex 94f6080..94f6080 100644 --- a/docs/public/showcase/tokyob0t-super-duper-hiper-mega-ultra-contribution.webp +++ b/docs/public/showcase/tokyob0t.webp diff --git a/docs/public/vitepress-logo-large.webp b/docs/public/vitepress-logo-large.webp Binary files differdeleted file mode 100644 index 2e47b15..0000000 --- a/docs/public/vitepress-logo-large.webp +++ /dev/null diff --git a/docs/showcases/showcases.ts b/docs/showcases/showcases.ts index 2aea04a..a673aaf 100644 --- a/docs/showcases/showcases.ts +++ b/docs/showcases/showcases.ts @@ -12,14 +12,14 @@ type Grid<T> = T | [T, T] export default [ { - image: "/astal/showcase/aylur.png", + image: "/astal/showcase/aylur.webp", url: "https://github.com/Aylur/dotfiles", icon: "devicon-typescript-plain", - description: "Placeholder (this is an ags v1 screenshot)", + title: "Marble Shell", author: "Aylur", }, { - image: "/astal/showcase/tokyob0t-super-duper-hiper-mega-ultra-contribution.webp", + image: "/astal/showcase/tokyob0t.webp", url: "https://github.com/tokyob0t/dotfiles", icon: "devicon-lua-plain", title: "Tokyob0t's Desktop", @@ -33,6 +33,20 @@ export default [ title: "kompass", author: "kotontrion", }, + { + image: "/astal/showcase/ezerinz.webp", + url: "https://github.com/ezerinz/epik-shell", + icon: "devicon-javascript-plain", + title: "Epik Shell", + author: "ezerinz", + }, + { + image: "/astal/showcase/hyprpanel_showcase.webp", + url: "https://github.com/Jas-SinghFSU/hyprpanel", + icon: "devicon-javascript-plain", + title: "HyprPanel", + author: "Jas", + }, // add more showcases here~ ] satisfies Array<Grid<Showcase>> diff --git a/examples/js/.gitignore b/examples/gtk3/js/.gitignore index d53b85b..d53b85b 100644 --- a/examples/js/.gitignore +++ b/examples/gtk3/js/.gitignore diff --git a/examples/js/applauncher/README.md b/examples/gtk3/js/applauncher/README.md index 682adf1..682adf1 100644 --- a/examples/js/applauncher/README.md +++ b/examples/gtk3/js/applauncher/README.md diff --git a/examples/js/applauncher/app.ts b/examples/gtk3/js/applauncher/app.ts index d6c9e1c..d6c9e1c 100644 --- a/examples/js/applauncher/app.ts +++ b/examples/gtk3/js/applauncher/app.ts diff --git a/examples/js/applauncher/style.scss b/examples/gtk3/js/applauncher/style.scss index ba13eed..ba13eed 100644 --- a/examples/js/applauncher/style.scss +++ b/examples/gtk3/js/applauncher/style.scss diff --git a/examples/js/applauncher/widget/Applauncher.scss b/examples/gtk3/js/applauncher/widget/Applauncher.scss index ae2453d..ae2453d 100644 --- a/examples/js/applauncher/widget/Applauncher.scss +++ b/examples/gtk3/js/applauncher/widget/Applauncher.scss diff --git a/examples/js/applauncher/widget/Applauncher.tsx b/examples/gtk3/js/applauncher/widget/Applauncher.tsx index c7bac68..8206250 100644 --- a/examples/js/applauncher/widget/Applauncher.tsx +++ b/examples/gtk3/js/applauncher/widget/Applauncher.tsx @@ -35,6 +35,7 @@ function AppButton({ app }: { app: Apps.Application }) { export default function Applauncher() { const { CENTER } = Gtk.Align const apps = new Apps.Apps() + const width = Variable(1000) const text = Variable("") const list = text(text => apps.fuzzy_query(text).slice(0, MAX_ITEMS)) @@ -49,13 +50,16 @@ export default function Applauncher() { exclusivity={Astal.Exclusivity.IGNORE} keymode={Astal.Keymode.ON_DEMAND} application={App} - onShow={() => text.set("")} + onShow={(self) => { + text.set("") + width.set(self.get_current_monitor().workarea.width) + }} onKeyPressEvent={function (self, event: Gdk.Event) { if (event.get_keyval()[1] === Gdk.KEY_Escape) self.hide() }}> <box> - <eventbox widthRequest={4000} expand onClick={hide} /> + <eventbox widthRequest={width(w => w / 2)} expand onClick={hide} /> <box hexpand={false} vertical> <eventbox heightRequest={100} onClick={hide} /> <box widthRequest={500} className="Applauncher" vertical> @@ -81,7 +85,7 @@ export default function Applauncher() { </box> <eventbox expand onClick={hide} /> </box> - <eventbox widthRequest={4000} expand onClick={hide} /> + <eventbox widthRequest={width(w => w / 2)} expand onClick={hide} /> </box> </window> } diff --git a/examples/js/media-player/README.md b/examples/gtk3/js/media-player/README.md index 4e3d237..4e3d237 100644 --- a/examples/js/media-player/README.md +++ b/examples/gtk3/js/media-player/README.md diff --git a/examples/js/media-player/app.ts b/examples/gtk3/js/media-player/app.ts index 5b7558a..5b7558a 100644 --- a/examples/js/media-player/app.ts +++ b/examples/gtk3/js/media-player/app.ts diff --git a/examples/js/media-player/style.scss b/examples/gtk3/js/media-player/style.scss index 2e2f625..2e2f625 100644 --- a/examples/js/media-player/style.scss +++ b/examples/gtk3/js/media-player/style.scss diff --git a/examples/js/media-player/widget/MediaPlayer.scss b/examples/gtk3/js/media-player/widget/MediaPlayer.scss index e1597c2..e1597c2 100644 --- a/examples/js/media-player/widget/MediaPlayer.scss +++ b/examples/gtk3/js/media-player/widget/MediaPlayer.scss diff --git a/examples/js/media-player/widget/MediaPlayer.tsx b/examples/gtk3/js/media-player/widget/MediaPlayer.tsx index 06c7e77..06c7e77 100644 --- a/examples/js/media-player/widget/MediaPlayer.tsx +++ b/examples/gtk3/js/media-player/widget/MediaPlayer.tsx diff --git a/examples/js/notifications/README.md b/examples/gtk3/js/notifications/README.md index 60dad60..60dad60 100644 --- a/examples/js/notifications/README.md +++ b/examples/gtk3/js/notifications/README.md diff --git a/examples/js/notifications/app.ts b/examples/gtk3/js/notifications/app.ts index ed53292..ed53292 100644 --- a/examples/js/notifications/app.ts +++ b/examples/gtk3/js/notifications/app.ts diff --git a/examples/js/notifications/notifications/Notification.scss b/examples/gtk3/js/notifications/notifications/Notification.scss index a32f08b..a32f08b 100644 --- a/examples/js/notifications/notifications/Notification.scss +++ b/examples/gtk3/js/notifications/notifications/Notification.scss diff --git a/examples/js/notifications/notifications/Notification.tsx b/examples/gtk3/js/notifications/notifications/Notification.tsx index 5149d5b..5149d5b 100644 --- a/examples/js/notifications/notifications/Notification.tsx +++ b/examples/gtk3/js/notifications/notifications/Notification.tsx diff --git a/examples/js/notifications/notifications/NotificationPopups.tsx b/examples/gtk3/js/notifications/notifications/NotificationPopups.tsx index 9b84d84..13fdd88 100644 --- a/examples/js/notifications/notifications/NotificationPopups.tsx +++ b/examples/gtk3/js/notifications/notifications/NotificationPopups.tsx @@ -98,7 +98,7 @@ export default function NotificationPopups(gdkmonitor: Gdk.Monitor) { gdkmonitor={gdkmonitor} exclusivity={Astal.Exclusivity.EXCLUSIVE} anchor={TOP | RIGHT}> - <box vertical> + <box vertical noImplicitDestroy> {bind(notifs)} </box> </window> diff --git a/examples/js/notifications/style.scss b/examples/gtk3/js/notifications/style.scss index 7ef0168..7ef0168 100644 --- a/examples/js/notifications/style.scss +++ b/examples/gtk3/js/notifications/style.scss diff --git a/examples/gtk3/js/osd/README.md b/examples/gtk3/js/osd/README.md new file mode 100644 index 0000000..ee1d497 --- /dev/null +++ b/examples/gtk3/js/osd/README.md @@ -0,0 +1,7 @@ +# On Screen Display + + + +A simple widget that pops up when screen brightness or audio changes + +Uses the [WirePlumber library](https://aylur.github.io/astal/guide/libraries/wireplumber). diff --git a/examples/gtk3/js/osd/app.ts b/examples/gtk3/js/osd/app.ts new file mode 100644 index 0000000..50e314e --- /dev/null +++ b/examples/gtk3/js/osd/app.ts @@ -0,0 +1,11 @@ +import { App } from "astal/gtk3" +import style from "./style.scss" +import OSD from "./osd/OSD" + +App.start({ + instanceName: "osd-example", + css: style, + main() { + App.get_monitors().map(OSD) + }, +}) diff --git a/examples/gtk3/js/osd/osd/OSD.scss b/examples/gtk3/js/osd/osd/OSD.scss new file mode 100644 index 0000000..d0fe4d1 --- /dev/null +++ b/examples/gtk3/js/osd/osd/OSD.scss @@ -0,0 +1,30 @@ +$fg-color: #{"@theme_fg_color"}; +$bg-color: #{"@theme_bg_color"}; + +window.OSD { + box.OSD { + border-radius: 100px; + background-color: $bg-color; + padding: 13px 16px; + margin: 13px; + box-shadow: 3px 3px 7px 0 rgba(0,0,0,.4); + } + + icon { + font-size: 4rem; + } + + label { + font-size: 2.4rem; + } + + levelbar { + trough { + margin: 1 .6rem; + } + + block { + min-height: 2rem; + } + } +} diff --git a/examples/gtk3/js/osd/osd/OSD.tsx b/examples/gtk3/js/osd/osd/OSD.tsx new file mode 100644 index 0000000..df28da5 --- /dev/null +++ b/examples/gtk3/js/osd/osd/OSD.tsx @@ -0,0 +1,69 @@ +import { App, Astal, Gdk, Gtk } from "astal/gtk3" +import { timeout } from "astal/time" +import Variable from "astal/variable" +import Brightness from "./brightness" +import Wp from "gi://AstalWp" + +function OnScreenProgress({ visible }: { visible: Variable<boolean> }) { + const brightness = Brightness.get_default() + const speaker = Wp.get_default()!.get_default_speaker() + + const iconName = Variable("") + const value = Variable(0) + + let count = 0 + function show(v: number, icon: string) { + visible.set(true) + value.set(v) + iconName.set(icon) + count++ + timeout(2000, () => { + count-- + if (count === 0) visible.set(false) + }) + } + + return ( + <revealer + setup={(self) => { + self.hook(brightness, "notify::screen", () => + show(brightness.screen, "display-brightness-symbolic"), + ) + + if (speaker) { + self.hook(speaker, "notify::volume", () => + show(speaker.volume, speaker.volumeIcon), + ) + } + }} + revealChild={visible()} + transitionType={Gtk.RevealerTransitionType.SLIDE_UP} + > + <box className="OSD"> + <icon icon={iconName()} /> + <levelbar valign={Gtk.Align.CENTER} widthRequest={100} value={value()} /> + <label label={value(v => `${Math.floor(v * 100)}%`)} /> + </box> + </revealer> + ) +} + +export default function OSD(monitor: Gdk.Monitor) { + const visible = Variable(false) + + return ( + <window + gdkmonitor={monitor} + className="OSD" + namespace="osd" + application={App} + layer={Astal.Layer.OVERLAY} + keymode={Astal.Keymode.ON_DEMAND} + anchor={Astal.WindowAnchor.BOTTOM} + > + <eventbox onClick={() => visible.set(false)}> + <OnScreenProgress visible={visible} /> + </eventbox> + </window> + ) +} diff --git a/examples/gtk3/js/osd/osd/brightness.ts b/examples/gtk3/js/osd/osd/brightness.ts new file mode 100644 index 0000000..cf5060a --- /dev/null +++ b/examples/gtk3/js/osd/osd/brightness.ts @@ -0,0 +1,45 @@ +import GObject, { register, property } from "astal/gobject" +import { monitorFile, readFileAsync } from "astal/file" +import { exec, execAsync } from "astal/process" + +const get = (args: string) => Number(exec(`brightnessctl ${args}`)) +const screen = exec(`bash -c "ls -w1 /sys/class/backlight | head -1"`) + +@register({ GTypeName: "Brightness" }) +export default class Brightness extends GObject.Object { + static instance: Brightness + static get_default() { + if (!this.instance) + this.instance = new Brightness() + + return this.instance + } + + #screenMax = get("max") + #screen = get("get") / (get("max") || 1) + + @property(Number) + get screen() { return this.#screen } + + set screen(percent) { + if (percent < 0) + percent = 0 + + if (percent > 1) + percent = 1 + + execAsync(`brightnessctl set ${Math.floor(percent * 100)}% -q`).then(() => { + this.#screen = percent + this.notify("screen") + }) + } + + constructor() { + super() + monitorFile(`/sys/class/backlight/${screen}/brightness`, async f => { + const v = await readFileAsync(f) + this.#screen = Number(v) / this.#screenMax + this.notify("screen") + }) + } +} diff --git a/examples/gtk3/js/osd/style.scss b/examples/gtk3/js/osd/style.scss new file mode 100644 index 0000000..ba6f06d --- /dev/null +++ b/examples/gtk3/js/osd/style.scss @@ -0,0 +1 @@ +@use "./osd/OSD.scss"; diff --git a/examples/gtk3/js/popover/Popover.tsx b/examples/gtk3/js/popover/Popover.tsx new file mode 100644 index 0000000..38ea01e --- /dev/null +++ b/examples/gtk3/js/popover/Popover.tsx @@ -0,0 +1,90 @@ +import { Astal, Gdk, Gtk, Widget } from "astal/gtk3" + +const { TOP, BOTTOM, LEFT, RIGHT } = Astal.WindowAnchor + +type PopoverProps = Pick< + Widget.WindowProps, + | "name" + | "namespace" + | "className" + | "visible" + | "child" + | "marginBottom" + | "marginTop" + | "marginLeft" + | "marginRight" + | "halign" + | "valign" +> & { + onClose?(self: Widget.Window): void +} + +/** + * Full screen window widget where you can space the child widget + * using margins and alignment properties. + * + * NOTE: Child widgets will assume they can span across the full window width + * this means that setting `wrap` or `ellipsize` on labels for example will not work + * without explicitly setting its `max_width_chars` property. + * For a workaround see Popover2.tsx + */ +export default function Popover({ + child, + marginBottom, + marginTop, + marginLeft, + marginRight, + halign = Gtk.Align.CENTER, + valign = Gtk.Align.CENTER, + onClose, + ...props +}: PopoverProps) { + return ( + <window + {...props} + css="background-color: transparent" + keymode={Astal.Keymode.EXCLUSIVE} + anchor={TOP | BOTTOM | LEFT | RIGHT} + exclusivity={Astal.Exclusivity.IGNORE} + onNotifyVisible={(self) => { + if (!self.visible) onClose?.(self) + }} + // close when click occurs otside of child + onButtonPressEvent={(self, event) => { + const [, _x, _y] = event.get_coords() + const { x, y, width, height } = self + .get_child()! + .get_allocation() + + const xOut = _x < x || _x > x + width + const yOut = _y < y || _y > y + height + + // clicked outside + if (xOut || yOut) { + self.visible = false + } + }} + // close when hitting Escape + onKeyPressEvent={(self, event: Gdk.Event) => { + if (event.get_keyval()[1] === Gdk.KEY_Escape) { + self.visible = false + } + }} + > + <box + // make sure click event does not bubble up + onButtonPressEvent={() => true} + // child can be positioned with `halign` `valign` and margins + expand + halign={halign} + valign={valign} + marginBottom={marginBottom} + marginTop={marginTop} + marginStart={marginLeft} + marginEnd={marginRight} + > + {child} + </box> + </window> + ) +} diff --git a/examples/gtk3/js/popover/Popover2.tsx b/examples/gtk3/js/popover/Popover2.tsx new file mode 100644 index 0000000..e058079 --- /dev/null +++ b/examples/gtk3/js/popover/Popover2.tsx @@ -0,0 +1,66 @@ +import { Astal, Gdk, Widget } from "astal/gtk3" +import Variable from "astal/variable" + +type Popover2Props = Pick< + Widget.WindowProps, + | "name" + | "namespace" + | "className" + | "visible" + | "child" +> & { + onClose?(self: Widget.Window): void +} + +/** + * Full screen window where the child is positioned to center. + * + * NOTE: Workaround for the label wrap issue by padding the window + * with eventboxes and only anchoring to TOP | BOTTOM. + */ +export default function Popover2({ + child, + onClose, + ...props +}: Popover2Props) { + let win: Widget.Window + + const width = Variable(1000) + const hide = () => (win.visible = false) + + return ( + <window + {...props} + setup={self => win = self} + css="background-color: transparent" + keymode={Astal.Keymode.EXCLUSIVE} + anchor={Astal.WindowAnchor.TOP | Astal.WindowAnchor.BOTTOM} + exclusivity={Astal.Exclusivity.IGNORE} + onNotifyVisible={(self) => { + // instead of anchoring to all sides we set the width explicitly + // otherwise label wrapping won't work correctly without setting their width + if (self.visible) { + width.set(self.get_current_monitor().workarea.width) + } else { + onClose?.(self) + } + }} + // close when hitting Escape + onKeyPressEvent={(self, event: Gdk.Event) => { + if (event.get_keyval()[1] === Gdk.KEY_Escape) { + self.visible = false + } + }} + > + <box> + <eventbox widthRequest={width(w => w / 2)} expand onClick={hide} /> + <box hexpand={false} vertical> + <eventbox expand onClick={hide} /> + {child} + <eventbox expand onClick={hide} /> + </box> + <eventbox widthRequest={width(w => w / 2)} expand onClick={hide} /> + </box> + </window> + ) +} diff --git a/examples/gtk3/js/popover/app.tsx b/examples/gtk3/js/popover/app.tsx new file mode 100644 index 0000000..5386b66 --- /dev/null +++ b/examples/gtk3/js/popover/app.tsx @@ -0,0 +1,72 @@ +import { App, Astal, Gtk } from "astal/gtk3" +import { Variable } from "astal" +import Popover from "./Popover" +import Popover2 from "./Popover2" +const { TOP, RIGHT, LEFT } = Astal.WindowAnchor + +const lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean quis semper risus." + +App.start({ + instanceName: "popup-example", + css: ` + .popup { + background-color: @theme_bg_color; + box-shadow: 2px 3px 7px 0 rgba(0,0,0,0.4); + border-radius: 12px; + padding: 12px; + } + `, + main() { + const visible1 = Variable(false); + const visible2 = Variable(false); + + const _popover1 = <Popover + className="Popup" + onClose={() => visible1.set(false)} + visible={visible1()} + marginTop={36} + marginRight={60} + valign={Gtk.Align.START} + halign={Gtk.Align.END} + > + <box className="popup" vertical> + {/* maxWidthChars is needed to make wrap work */} + <label label={lorem} wrap maxWidthChars={8} /> + <button onClicked={() => visible1.set(false)}> + Click me to close the popup + </button> + </box> + </Popover> + + + const _popover2 = <Popover2 + className="Popup" + onClose={() => visible2.set(false)} + visible={visible2()} + > + <box className="popup" vertical> + {/* maxWidthChars is needed, wrap will work as intended */} + <label label={lorem} wrap /> + <button onClicked={() => visible2.set(false)}> + Click me to close the popup + </button> + </box> + </Popover2> + + return ( + <window + anchor={TOP | LEFT | RIGHT} + exclusivity={Astal.Exclusivity.EXCLUSIVE} + > + <box halign={Gtk.Align.END}> + <button onClicked={() => visible2.set(true)} halign={Gtk.Align.END}> + Click to open popover2 + </button> + <button onClicked={() => visible1.set(true)} halign={Gtk.Align.END}> + Click to open popover + </button> + </box> + </window> + ) + }, +}) diff --git a/examples/js/simple-bar/README.md b/examples/gtk3/js/simple-bar/README.md index f92b20e..f92b20e 100644 --- a/examples/js/simple-bar/README.md +++ b/examples/gtk3/js/simple-bar/README.md diff --git a/examples/js/simple-bar/app.ts b/examples/gtk3/js/simple-bar/app.ts index 4b7ea48..4b7ea48 100644 --- a/examples/js/simple-bar/app.ts +++ b/examples/gtk3/js/simple-bar/app.ts diff --git a/examples/vala/simple-bar/style.scss b/examples/gtk3/js/simple-bar/style.scss index 1dcf729..f5f771a 100644 --- a/examples/vala/simple-bar/style.scss +++ b/examples/gtk3/js/simple-bar/style.scss @@ -13,31 +13,45 @@ window.Bar { font-size: 1.1em; font-weight: bold; - button { - all: unset; - background-color: transparent; + label { + margin: 0 8px; + } + + .Workspaces { + button { + all: unset; + background-color: transparent; - &:hover label { - background-color: color.adjust($fg, $alpha: -0.84); - border-color: color.adjust($accent, $alpha: -0.8); + &:hover label { + background-color: color.adjust($fg, $alpha: -0.84); + border-color: color.adjust($accent, $alpha: -0.8); + } + + &:active label { + background-color: color.adjust($fg, $alpha: -0.8) + } } - &:active label { - background-color: color.adjust($fg, $alpha: -0.8) + label { + transition: 200ms; + padding: 0 8px; + margin: 2px; + border-radius: $radius; + border: 1pt solid transparent; } - } - label { - transition: 200ms; - padding: 0 8px; - margin: 2px; - border-radius: $radius; - border: 1pt solid transparent; + .focused label { + color: $accent; + border-color: $accent; + } } - .Workspaces .focused label { - color: $accent; - border-color: $accent; + .SysTray { + margin-right: 8px; + + button { + padding: 0 4px; + } } .FocusedClient { diff --git a/examples/js/simple-bar/widget/Bar.tsx b/examples/gtk3/js/simple-bar/widget/Bar.tsx index 8a0126e..6592f6a 100644 --- a/examples/js/simple-bar/widget/Bar.tsx +++ b/examples/gtk3/js/simple-bar/widget/Bar.tsx @@ -11,33 +11,33 @@ import Tray from "gi://AstalTray" function SysTray() { const tray = Tray.get_default() - return <box> - {bind(tray, "items").as(items => items.map(item => { - if (item.iconThemePath) - App.add_icons(item.iconThemePath) - - const menu = item.create_menu() - - return <button + return <box className="SysTray"> + {bind(tray, "items").as(items => items.map(item => ( + <menubutton tooltipMarkup={bind(item, "tooltipMarkup")} - onDestroy={() => menu?.destroy()} - onClickRelease={self => { - menu?.popup_at_widget(self, Gdk.Gravity.SOUTH, Gdk.Gravity.NORTH, null) - }}> - <icon gIcon={bind(item, "gicon")} /> - </button> - }))} + usePopover={false} + actionGroup={bind(item, "actionGroup").as(ag => ["dbusmenu", ag])} + menuModel={bind(item, "menuModel")}> + <icon gicon={bind(item, "gicon")} /> + </menubutton> + )))} </box> } function Wifi() { - const { wifi } = Network.get_default() + const network = Network.get_default() + const wifi = bind(network, "wifi") + + return <box visible={wifi.as(Boolean)}> + {wifi.as(wifi => wifi && ( + <icon + tooltipText={bind(wifi, "ssid").as(String)} + className="Wifi" + icon={bind(wifi, "iconName")} + /> + ))} + </box> - return <icon - tooltipText={bind(wifi, "ssid").as(String)} - className="Wifi" - icon={bind(wifi, "iconName")} - /> } function AudioSlider() { @@ -95,6 +95,7 @@ function Workspaces() { return <box className="Workspaces"> {bind(hypr, "workspaces").as(wss => wss + .filter(ws => !(ws.id >= -99 && ws.id <= -2)) // filter out special workspaces .sort((a, b) => a.id - b.id) .map(ws => ( <button diff --git a/examples/gtk3/lua/applauncher/README.md b/examples/gtk3/lua/applauncher/README.md new file mode 100644 index 0000000..682adf1 --- /dev/null +++ b/examples/gtk3/lua/applauncher/README.md @@ -0,0 +1,5 @@ +# Applauncher + + + +Using [Apps](https://aylur.github.io/astal/guide/libraries/apps). diff --git a/examples/gtk3/lua/applauncher/init.lua b/examples/gtk3/lua/applauncher/init.lua new file mode 100644 index 0000000..0cd6db5 --- /dev/null +++ b/examples/gtk3/lua/applauncher/init.lua @@ -0,0 +1,16 @@ +local astal = require("astal") +local App = require("astal.gtk3.app") + +local AppLauncher = require("widget.Applauncher") +local src = require("lib").src + +local scss = src("style.scss") +local css = "/tmp/style.css" + +astal.exec("sass " .. scss .. " " .. css) + +App:start({ + instance_name = "launcher", + css = css, + main = AppLauncher, +}) diff --git a/examples/gtk3/lua/applauncher/lib.lua b/examples/gtk3/lua/applauncher/lib.lua new file mode 100644 index 0000000..8a50bdd --- /dev/null +++ b/examples/gtk3/lua/applauncher/lib.lua @@ -0,0 +1,38 @@ +local M = {} + +function M.src(path) + local str = debug.getinfo(2, "S").source:sub(2) + local src = str:match("(.*/)") or str:match("(.*\\)") or "./" + return src .. path +end + +---@generic T, R +---@param array T[] +---@param func fun(T, i: integer): R +---@return R[] +function M.map(array, func) + local new_arr = {} + for i, v in ipairs(array) do + new_arr[i] = func(v, i) + end + return new_arr +end + +---@generic T +---@param array T[] +---@param start integer +---@param stop? integer +---@return T[] +function M.slice(array, start, stop) + local new_arr = {} + + stop = stop or #array + + for i = start, stop do + table.insert(new_arr, array[i]) + end + + return new_arr +end + +return M diff --git a/examples/gtk3/lua/applauncher/style.scss b/examples/gtk3/lua/applauncher/style.scss new file mode 100644 index 0000000..ba13eed --- /dev/null +++ b/examples/gtk3/lua/applauncher/style.scss @@ -0,0 +1 @@ +@use "./widget/Applauncher.scss" diff --git a/examples/gtk3/lua/applauncher/widget/Applauncher.lua b/examples/gtk3/lua/applauncher/widget/Applauncher.lua new file mode 100644 index 0000000..78f7fa5 --- /dev/null +++ b/examples/gtk3/lua/applauncher/widget/Applauncher.lua @@ -0,0 +1,118 @@ +local astal = require("astal") + +local Apps = astal.require("AstalApps") +local App = require("astal.gtk3.app") +local Astal = require("astal.gtk3").Astal +local Gdk = require("astal.gtk3").Gdk +local Variable = astal.Variable +local Widget = require("astal.gtk3.widget") + +local slice = require("lib").slice +local map = require("lib").map + +local MAX_ITEMS = 8 + +local function hide() + local launcher = App:get_window("launcher") + if launcher then launcher:hide() end +end + +local function AppButton(app) + return Widget.Button({ + class_name = "AppButton", + on_clicked = function() + hide() + app:launch() + end, + Widget.Box({ + Widget.Icon({ icon = app.icon_name }), + Widget.Box({ + valign = "CENTER", + vertical = true, + Widget.Label({ + class_name = "name", + wrap = true, + xalign = 0, + label = app.name, + }), + app.description and Widget.Label({ + class_name = "description", + wrap = true, + xalign = 0, + label = app.description, + }), + }), + }), + }) +end + +return function() + local apps = Apps.Apps() + + local text = Variable("") + local list = text( + function(t) return slice(apps:fuzzy_query(t), 1, MAX_ITEMS) end + ) + + local on_enter = function() + local found = apps:fuzzy_query(text:get())[1] + if found then + found:launch() + hide() + end + end + + return Widget.Window({ + name = "launcher", + anchor = Astal.WindowAnchor.TOP + Astal.WindowAnchor.BOTTOM, + exclusivity = "IGNORE", + keymode = "ON_DEMAND", + application = App, + on_show = function() text:set("") end, + on_key_press_event = function(self, event) + if event.keyval == Gdk.KEY_Escape then self:hide() end + end, + Widget.Box({ + Widget.EventBox({ + expand = true, + on_click = hide, + width_request = 4000, + }), + Widget.Box({ + hexpand = false, + vertical = true, + Widget.EventBox({ on_click = hide, height_request = 100 }), + Widget.Box({ + vertical = true, + width_request = 500, + class_name = "Applauncher", + Widget.Entry({ + placeholder_text = "Search", + text = text(), + on_changed = function(self) text:set(self.text) end, + on_activate = on_enter, + }), + Widget.Box({ + spacing = 6, + vertical = true, + list:as(function(l) return map(l, AppButton) end), + }), + Widget.Box({ + halign = "CENTER", + class_name = "not-found", + vertical = true, + visible = list:as(function(l) return #l == 0 end), + Widget.Icon({ icon = "system-search-symbolic" }), + Widget.Label({ label = "No match found" }), + }), + }), + Widget.EventBox({ expand = true, on_click = hide }), + }), + Widget.EventBox({ + width_request = 4000, + expand = true, + on_click = hide, + }), + }), + }) +end diff --git a/examples/gtk3/lua/applauncher/widget/Applauncher.scss b/examples/gtk3/lua/applauncher/widget/Applauncher.scss new file mode 100644 index 0000000..38b5be1 --- /dev/null +++ b/examples/gtk3/lua/applauncher/widget/Applauncher.scss @@ -0,0 +1,59 @@ +@use 'sass:string'; + +@function gtkalpha($c, $a) { + @return string.unquote('alpha(#{$c},#{$a})'); +} + +// https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-3-24/gtk/theme/Adwaita/_colors-public.scss +$fg-color: #{'@theme_fg_color'}; +$bg-color: #{'@theme_bg_color'}; + +window#launcher { + all: unset; + + box.Applauncher { + background-color: $bg-color; + border-radius: 11px; + margin: 1rem; + padding: 0.8rem; + box-shadow: 2px 3px 8px 0 gtkalpha(black, 0.4); + + entry { + margin-bottom: 0.8rem; + } + + button { + min-width: 0; + min-height: 0; + padding: 0.5rem; + + icon { + font-size: 3em; + margin-right: 0.3rem; + } + + label.name { + font-weight: bold; + font-size: 1.1em; + } + + label.description { + color: gtkalpha($fg-color, 0.8); + } + } + + box.not-found { + padding: 1rem; + + icon { + font-size: 6em; + color: gtkalpha($fg-color, 0.7); + } + + label { + color: gtkalpha($fg-color, 0.9); + font-size: 1.2em; + } + } + } +} diff --git a/examples/gtk3/lua/media-player/README.md b/examples/gtk3/lua/media-player/README.md new file mode 100644 index 0000000..4e3d237 --- /dev/null +++ b/examples/gtk3/lua/media-player/README.md @@ -0,0 +1,5 @@ +# Media Player + + + +Using [Mpris](https://aylur.github.io/astal/guide/libraries/mpris). diff --git a/examples/gtk3/lua/media-player/init.lua b/examples/gtk3/lua/media-player/init.lua new file mode 100644 index 0000000..16ccbfb --- /dev/null +++ b/examples/gtk3/lua/media-player/init.lua @@ -0,0 +1,19 @@ +local astal = require("astal") +local App = require("astal.gtk3.app") +local Widget = require("astal.gtk3.widget") + +local MprisPlayers = require("widget.MediaPlayer") +local src = require("lib").src + +local scss = src("style.scss") +local css = "/tmp/style.css" + +astal.exec("sass " .. scss .. " " .. css) + +App:start({ + instance_name = "lua", + css = css, + main = function() + Widget.Window({ MprisPlayers() }) + end, +}) diff --git a/examples/gtk3/lua/media-player/lib.lua b/examples/gtk3/lua/media-player/lib.lua new file mode 100644 index 0000000..8a50bdd --- /dev/null +++ b/examples/gtk3/lua/media-player/lib.lua @@ -0,0 +1,38 @@ +local M = {} + +function M.src(path) + local str = debug.getinfo(2, "S").source:sub(2) + local src = str:match("(.*/)") or str:match("(.*\\)") or "./" + return src .. path +end + +---@generic T, R +---@param array T[] +---@param func fun(T, i: integer): R +---@return R[] +function M.map(array, func) + local new_arr = {} + for i, v in ipairs(array) do + new_arr[i] = func(v, i) + end + return new_arr +end + +---@generic T +---@param array T[] +---@param start integer +---@param stop? integer +---@return T[] +function M.slice(array, start, stop) + local new_arr = {} + + stop = stop or #array + + for i = start, stop do + table.insert(new_arr, array[i]) + end + + return new_arr +end + +return M diff --git a/examples/gtk3/lua/media-player/style.scss b/examples/gtk3/lua/media-player/style.scss new file mode 100644 index 0000000..be398dd --- /dev/null +++ b/examples/gtk3/lua/media-player/style.scss @@ -0,0 +1 @@ +@use './widget/MediaPlayer.scss'; diff --git a/examples/gtk3/lua/media-player/widget/MediaPlayer.lua b/examples/gtk3/lua/media-player/widget/MediaPlayer.lua new file mode 100644 index 0000000..fbad3e0 --- /dev/null +++ b/examples/gtk3/lua/media-player/widget/MediaPlayer.lua @@ -0,0 +1,144 @@ +local astal = require("astal") + +local Astal = astal.require("Astal", "3.0") + +local bind = astal.bind +local Widget = require("astal.gtk3.widget") +local lookup_icon = Astal.Icon.lookup_icon + +local map = require("lib").map + +local Mpris = astal.require("AstalMpris") + +---@param length integer +local function length_str(length) + local min = math.floor(length / 60) + local sec = math.floor(length % 60) + + return string.format("%d:%s%d", min, sec < 10 and "0" or "", sec) +end + +local function MediaPlayer(player) + local title = bind(player, "title"):as( + function(t) return t or "Unknown Track" end + ) + + local artist = bind(player, "artist"):as( + function(a) return a or "Unknown Artist" end + ) + + local cover_art = bind(player, "cover-art"):as( + function(c) return string.format("background-image: url('%s');", c) end + ) + + local player_icon = bind(player, "entry"):as( + function(e) return lookup_icon(e) and e or "audio-x-generic-symbolic" end + ) + + local position = bind(player, "position"):as( + function(p) return player.length > 0 and p / player.length or 0 end + ) + + local play_icon = bind(player, "playback-status"):as( + function(s) + return s == "PLAYING" and "media-playback-pause-symbolic" + or "media-playback-start-symbolic" + end + ) + + return Widget.Box({ + class_name = "MediaPlayer", + Widget.Box({ + class_name = "cover-art", + css = cover_art, + }), + Widget.Box({ + vertical = true, + Widget.Box({ + class_name = "title", + Widget.Label({ + ellipsize = "END", + hexpand = true, + halign = "START", + label = title, + }), + Widget.Icon({ + icon = player_icon, + }), + }), + Widget.Label({ + halign = "START", + valign = "START", + vexpand = true, + wrap = true, + label = artist, + }), + Widget.Slider({ + visible = bind(player, "length"):as( + function(l) return l > 0 end + ), + on_dragged = function(event) + player.position = event.value * player.length + end, + value = position, + }), + Widget.CenterBox({ + class_name = "actions", + Widget.Label({ + hexpand = true, + class_name = "position", + halign = "START", + visible = bind(player, "length"):as( + function(l) return l > 0 end + ), + label = bind(player, "position"):as(length_str), + }), + Widget.Box({ + Widget.Button({ + on_clicked = function() player:previous() end, + visible = bind(player, "can-go-previous"), + Widget.Icon({ + icon = "media-skip-backward-symbolic", + }), + }), + Widget.Button({ + on_clicked = function() player:play_pause() end, + visible = bind(player, "can-control"), + Widget.Icon({ + icon = play_icon, + }), + }), + Widget.Button({ + on_clicked = function() player:next() end, + visible = bind(player, "can-go-next"), + Widget.Icon({ + icon = "media-skip-forward-symbolic", + }), + }), + }), + Widget.Label({ + class_name = "length", + hexpand = true, + halign = "END", + visible = bind(player, "length"):as( + function(l) return l > 0 end + ), + label = bind(player, "length"):as( + function(l) return l > 0 and length_str(l) or "0:00" end + ), + }), + }), + }), + }) +end + +return function() + local mpris = Mpris.get_default() + + return Widget.Box({ + vertical = true, + bind(mpris, "players"):as( + function(players) return map(players, MediaPlayer) end + ), + }) +end diff --git a/examples/gtk3/lua/media-player/widget/MediaPlayer.scss b/examples/gtk3/lua/media-player/widget/MediaPlayer.scss new file mode 100644 index 0000000..e1597c2 --- /dev/null +++ b/examples/gtk3/lua/media-player/widget/MediaPlayer.scss @@ -0,0 +1,56 @@ +// https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-3-24/gtk/theme/Adwaita/_colors-public.scss +$fg-color: #{"@theme_fg_color"}; +$bg-color: #{"@theme_bg_color"}; + +window { + all: unset; +} + +box.MediaPlayer { + padding: .6rem; + background-color: $bg-color; + + box.cover-art { + min-width: 120px; + min-height: 120px; + border-radius: 9px; + margin-right: .6rem; + background-size: contain; + background-position: center; + } + + box.title { + label { + font-weight: bold; + font-size: 1.1em; + } + } + + scale { + padding: 0; + margin: .4rem 0; + + trough { + min-height: 8px; + } + + highlight { + background-color: $fg-color; + } + + slider { + all: unset; + } + } + + centerbox.actions { + min-width: 220px; + + button { + min-width: 0; + min-height: 0; + padding: .4rem; + margin: 0 .2rem; + } + } +} diff --git a/examples/gtk3/lua/notifications/README.md b/examples/gtk3/lua/notifications/README.md new file mode 100644 index 0000000..60dad60 --- /dev/null +++ b/examples/gtk3/lua/notifications/README.md @@ -0,0 +1,5 @@ +# Notifications Popups + + + +A replacement for dunst and other daemons using [Notifd](https://aylur.github.io/astal/guide/libraries/notifd). diff --git a/examples/gtk3/lua/notifications/init.lua b/examples/gtk3/lua/notifications/init.lua new file mode 100644 index 0000000..886e9ab --- /dev/null +++ b/examples/gtk3/lua/notifications/init.lua @@ -0,0 +1,20 @@ +local astal = require("astal") +local App = require("astal.gtk3.app") + +local NotificationPopups = require("notifications.NotificationPopups") +local src = require("lib").src + +local scss = src("style.scss") +local css = "/tmp/style.css" + +astal.exec("sass " .. scss .. " " .. css) + +App:start({ + instance_name = "notifications", + css = css, + main = function() + for _, mon in pairs(App.monitors) do + NotificationPopups(mon) + end + end, +}) diff --git a/examples/gtk3/lua/notifications/lib.lua b/examples/gtk3/lua/notifications/lib.lua new file mode 100644 index 0000000..289fc7e --- /dev/null +++ b/examples/gtk3/lua/notifications/lib.lua @@ -0,0 +1,74 @@ +local astal = require("astal") +local Variable = require("astal").Variable +local Gtk = require("astal.gtk3").Gtk +local GLib = astal.require("GLib") + +local M = {} + +function M.src(path) + local str = debug.getinfo(2, "S").source:sub(2) + local src = str:match("(.*/)") or str:match("(.*\\)") or "./" + return src .. path +end + +---@generic T, R +---@param array T[] +---@param func fun(T, i: integer): R +---@return R[] +function M.map(array, func) + local new_arr = {} + for i, v in ipairs(array) do + new_arr[i] = func(v, i) + end + return new_arr +end + +---@param path string +---@return boolean +function M.file_exists(path) return GLib.file_test(path, "EXISTS") end + +function M.varmap(initial) + local map = initial + local var = Variable() + + local function notify() + local arr = {} + for _, value in pairs(map) do + table.insert(arr, value) + end + var:set(arr) + end + + local function delete(key) + if Gtk.Widget:is_type_of(map[key]) then map[key]:destroy() end + + map[key] = nil + end + + notify() + + return setmetatable({ + set = function(key, value) + delete(key) + map[key] = value + notify() + end, + delete = function(key) + delete(key) + notify() + end, + get = function() return var:get() end, + subscribe = function(callback) return var:subscribe(callback) end, + }, { + __call = function() return var() end, + }) +end + +---@param time number +---@param format? string +function M.time(time, format) + format = format or "%H:%M" + return GLib.DateTime.new_from_unix_local(time):format(format) +end + +return M diff --git a/examples/gtk3/lua/notifications/notifications/Notification.lua b/examples/gtk3/lua/notifications/notifications/Notification.lua new file mode 100644 index 0000000..39d36f5 --- /dev/null +++ b/examples/gtk3/lua/notifications/notifications/Notification.lua @@ -0,0 +1,105 @@ +local Widget = require("astal.gtk3").Widget +local Gtk = require("astal.gtk3").Gtk +local Astal = require("astal.gtk3").Astal + +local map = require("lib").map +local time = require("lib").time +local file_exists = require("lib").file_exists + +local function is_icon(icon) return Astal.Icon.lookup_icon(icon) ~= nil end + +---@param props { setup?: function, on_hover_lost?: function, notification: any } +return function(props) + local n = props.notification + + local header = Widget.Box({ + class_name = "header", + (n.app_icon or n.desktop_entry) and Widget.Icon({ + class_name = "app-icon", + icon = n.app_icon or n.desktop_entry, + }), + Widget.Label({ + class_name = "app-name", + halign = "START", + ellipsize = "END", + label = n.app_name or "Unknown", + }), + Widget.Label({ + class_name = "time", + hexpand = true, + halign = "END", + label = time(n.time), + }), + Widget.Button({ + on_clicked = function() n:dismiss() end, + Widget.Icon({ icon = "window-close-symbolic" }), + }), + }) + + local content = Widget.Box({ + class_name = "content", + (n.image and file_exists(n.image)) and Widget.Box({ + valign = "START", + class_name = "image", + css = string.format("background-image: url('%s')", n.image), + }), + n.image and is_icon(n.image) and Widget.Box({ + valign = "START", + class_name = "icon-image", + Widget.Icon({ + icon = n.image, + hexpand = true, + vexpand = true, + halign = "CENTER", + valign = "CENTER", + }), + }), + Widget.Box({ + vertical = true, + Widget.Label({ + class_name = "summary", + halign = "START", + xalign = 0, + ellipsize = "END", + label = n.summary, + }), + Widget.Label({ + class_name = "body", + wrap = true, + use_markup = true, + halign = "START", + xalign = 0, + justify = "FILL", + label = n.body, + }), + }), + }) + + return Widget.EventBox({ + class_name = string.format("Notification %s", string.lower(n.urgency)), + setup = props.setup, + on_hover_lost = props.on_hover_lost, + Widget.Box({ + vertical = true, + header, + Gtk.Separator({ visible = true }), + content, + #n.actions > 0 and Widget.Box({ + class_name = "actions", + map(n.actions, function(action) + local label, id = action.label, action.id + + return Widget.Button({ + hexpand = true, + on_clicked = function() return n:invoke(id) end, + Widget.Label({ + label = label, + halign = "CENTER", + hexpand = true, + }), + }) + end), + }), + }), + }) +end diff --git a/examples/gtk3/lua/notifications/notifications/Notification.scss b/examples/gtk3/lua/notifications/notifications/Notification.scss new file mode 100644 index 0000000..089d587 --- /dev/null +++ b/examples/gtk3/lua/notifications/notifications/Notification.scss @@ -0,0 +1,126 @@ +@use "sass:string"; + +@function gtkalpha($c, $a) { + @return string.unquote("alpha(#{$c},#{$a})"); +} + +// https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-3-24/gtk/theme/Adwaita/_colors-public.scss +$fg-color: #{"@theme_fg_color"}; +$bg-color: #{"@theme_bg_color"}; +$error: red; + +window.NotificationPopups { + all: unset; +} + +eventbox.Notification { + + &:first-child>box { + margin-top: 1rem; + } + + &:last-child>box { + margin-bottom: 1rem; + } + + // eventboxes can not take margins so we style its inner box instead + >box { + min-width: 400px; + border-radius: 13px; + background-color: $bg-color; + margin: .5rem 1rem .5rem 1rem; + box-shadow: 2px 3px 8px 0 gtkalpha(black, .4); + border: 1pt solid gtkalpha($fg-color, .03); + } + + &.critical>box { + border: 1pt solid gtkalpha($error, .4); + + .header { + + .app-name { + color: gtkalpha($error, .8); + + } + + .app-icon { + color: gtkalpha($error, .6); + } + } + } + + .header { + padding: .5rem; + color: gtkalpha($fg-color, 0.5); + + .app-icon { + margin: 0 .4rem; + } + + .app-name { + margin-right: .3rem; + font-weight: bold; + + &:first-child { + margin-left: .4rem; + } + } + + .time { + margin: 0 .4rem; + } + + button { + padding: .2rem; + min-width: 0; + min-height: 0; + } + } + + separator { + margin: 0 .4rem; + background-color: gtkalpha($fg-color, .1); + } + + .content { + margin: 1rem; + margin-top: .5rem; + + .summary { + font-size: 1.2em; + color: $fg-color; + } + + .body { + color: gtkalpha($fg-color, 0.8); + } + + .image { + border: 1px solid gtkalpha($fg-color, .02); + margin-right: .5rem; + border-radius: 9px; + min-width: 100px; + min-height: 100px; + background-size: cover; + background-position: center; + } + } + + .actions { + margin: 1rem; + margin-top: 0; + padding: .2rem; + + button { + margin: 0 .3rem; + + &:first-child { + margin-left: 0; + } + + &:last-child { + margin-right: 0; + } + } + } +} diff --git a/examples/gtk3/lua/notifications/notifications/NotificationPopups.lua b/examples/gtk3/lua/notifications/notifications/NotificationPopups.lua new file mode 100644 index 0000000..c5f9e1b --- /dev/null +++ b/examples/gtk3/lua/notifications/notifications/NotificationPopups.lua @@ -0,0 +1,57 @@ +local astal = require("astal") +local Widget = require("astal.gtk3").Widget + +local Notifd = astal.require("AstalNotifd") +local Notification = require("notifications.Notification") +local timeout = astal.timeout + +local TIMEOUT_DELAY = 5000 + +local varmap = require("lib").varmap +local notifd = Notifd.get_default() + +local function NotificationMap() + local notif_map = varmap({}) + + notifd.on_notified = function(_, id) + notif_map.set( + id, + Notification({ + notification = notifd:get_notification(id), + -- once hovering over the notification is done + -- destroy the widget without calling notification.dismiss() + -- so that it acts as a "popup" and we can still display it + -- in a notification center like widget + -- but clicking on the close button will close it + on_hover_lost = function() notif_map.delete(id) end, + setup = function() + timeout(TIMEOUT_DELAY, function() + -- uncomment this if you want to "hide" the notifications + -- after TIMEOUT_DELAY + + -- NotificationMap.delete(id) + end) + end, + }) + ) + end + + notifd.on_resolved = function(_, id) notif_map.delete(id) end + + return notif_map +end + +return function(gdkmonitor) + local Anchor = astal.require("Astal").WindowAnchor + local notifs = NotificationMap() + + return Widget.Window({ + class_name = "NotificationPopups", + gdkmonitor = gdkmonitor, + anchor = Anchor.TOP + Anchor.RIGHT, + Widget.Box({ + vertical = true, + notifs(), + }), + }) +end diff --git a/examples/gtk3/lua/notifications/style.scss b/examples/gtk3/lua/notifications/style.scss new file mode 100644 index 0000000..7ef0168 --- /dev/null +++ b/examples/gtk3/lua/notifications/style.scss @@ -0,0 +1 @@ +@use "./notifications/Notification.scss"; diff --git a/examples/lua/simple-bar/README.md b/examples/gtk3/lua/simple-bar/README.md index 48cc27c..48cc27c 100644 --- a/examples/lua/simple-bar/README.md +++ b/examples/gtk3/lua/simple-bar/README.md diff --git a/examples/lua/simple-bar/init.lua b/examples/gtk3/lua/simple-bar/init.lua index 8c412fb..8c412fb 100644 --- a/examples/lua/simple-bar/init.lua +++ b/examples/gtk3/lua/simple-bar/init.lua diff --git a/examples/lua/simple-bar/lib.lua b/examples/gtk3/lua/simple-bar/lib.lua index d94db5c..6f2dcea 100644 --- a/examples/lua/simple-bar/lib.lua +++ b/examples/gtk3/lua/simple-bar/lib.lua @@ -9,12 +9,12 @@ function M.src(path) end ---@generic T, R ----@param arr T[] ----@param func fun(T, integer): R +---@param array T[] +---@param func fun(T, i: integer): R ---@return R[] -function M.map(arr, func) +function M.map(array, func) local new_arr = {} - for i, v in ipairs(arr) do + for i, v in ipairs(array) do new_arr[i] = func(v, i) end return new_arr diff --git a/examples/lua/simple-bar/style.scss b/examples/gtk3/lua/simple-bar/style.scss index 1dcf729..f5f771a 100644 --- a/examples/lua/simple-bar/style.scss +++ b/examples/gtk3/lua/simple-bar/style.scss @@ -13,31 +13,45 @@ window.Bar { font-size: 1.1em; font-weight: bold; - button { - all: unset; - background-color: transparent; + label { + margin: 0 8px; + } + + .Workspaces { + button { + all: unset; + background-color: transparent; - &:hover label { - background-color: color.adjust($fg, $alpha: -0.84); - border-color: color.adjust($accent, $alpha: -0.8); + &:hover label { + background-color: color.adjust($fg, $alpha: -0.84); + border-color: color.adjust($accent, $alpha: -0.8); + } + + &:active label { + background-color: color.adjust($fg, $alpha: -0.8) + } } - &:active label { - background-color: color.adjust($fg, $alpha: -0.8) + label { + transition: 200ms; + padding: 0 8px; + margin: 2px; + border-radius: $radius; + border: 1pt solid transparent; } - } - label { - transition: 200ms; - padding: 0 8px; - margin: 2px; - border-radius: $radius; - border: 1pt solid transparent; + .focused label { + color: $accent; + border-color: $accent; + } } - .Workspaces .focused label { - color: $accent; - border-color: $accent; + .SysTray { + margin-right: 8px; + + button { + padding: 0 4px; + } } .FocusedClient { diff --git a/examples/lua/simple-bar/widget/Bar.lua b/examples/gtk3/lua/simple-bar/widget/Bar.lua index bf230bb..3f685a2 100644 --- a/examples/lua/simple-bar/widget/Bar.lua +++ b/examples/gtk3/lua/simple-bar/widget/Bar.lua @@ -1,8 +1,6 @@ local astal = require("astal") -local App = require("astal.gtk3.app") local Widget = require("astal.gtk3.widget") local Variable = astal.Variable -local Gdk = astal.require("Gdk", "3.0") local GLib = astal.require("GLib") local bind = astal.bind local Mpris = astal.require("AstalMpris") @@ -17,28 +15,18 @@ local function SysTray() local tray = Tray.get_default() return Widget.Box({ + class_name = "SysTray", bind(tray, "items"):as(function(items) return map(items, function(item) - if item.icon_theme_path ~= nil then - App:add_icons(item.icon_theme_path) - end - - local menu = item:create_menu() - - return Widget.Button({ + return Widget.MenuButton({ tooltip_markup = bind(item, "tooltip_markup"), - on_destroy = function() - if menu ~= nil then - menu:destroy() - end - end, - on_click_release = function(self) - if menu ~= nil then - menu:popup_at_widget(self, Gdk.Gravity.SOUTH, Gdk.Gravity.NORTH, nil) - end - end, + use_popover = false, + menu_model = bind(item, "menu-model"), + action_group = bind(item, "action-group"):as( + function(ag) return { "dbusmenu", ag } end + ), Widget.Icon({ - g_icon = bind(item, "gicon"), + gicon = bind(item, "gicon"), }), }) end) @@ -53,21 +41,32 @@ local function FocusedClient() return Widget.Box({ class_name = "Focused", visible = focused, - focused:as(function(client) - return client and Widget.Label({ - label = bind(client, "title"):as(tostring), - }) - end), + focused:as( + function(client) + return client + and Widget.Label({ + label = bind(client, "title"):as(tostring), + }) + end + ), }) end local function Wifi() - local wifi = Network.get_default().wifi + local network = Network.get_default() + local wifi = bind(network, "wifi") - return Widget.Icon({ - tooltip_text = bind(wifi, "ssid"):as(tostring), - class_name = "Wifi", - icon = bind(wifi, "icon-name"), + return Widget.Box({ + visible = wifi:as(function(v) return v ~= nil end), + wifi:as( + function(w) + return Widget.Icon({ + tooltip_text = bind(w, "ssid"):as(tostring), + class_name = "Wifi", + icon = bind(w, "icon-name"), + }) + end + ), }) end @@ -82,9 +81,7 @@ local function AudioSlider() }), Widget.Slider({ hexpand = true, - on_dragged = function(self) - speaker.volume = self.value - end, + on_dragged = function(self) speaker.volume = self.value end, value = bind(speaker, "volume"), }), }) @@ -100,9 +97,9 @@ local function BatteryLevel() icon = bind(bat, "battery-icon-name"), }), Widget.Label({ - label = bind(bat, "percentage"):as(function(p) - return tostring(math.floor(p * 100)) .. " %" - end), + label = bind(bat, "percentage"):as( + function(p) return tostring(math.floor(p * 100)) .. " %" end + ), }), }) end @@ -116,14 +113,20 @@ local function Media() Widget.Box({ class_name = "Cover", valign = "CENTER", - css = bind(player, "cover-art"):as(function(cover) - return "background-image: url('" .. (cover or "") .. "');" - end), + css = bind(player, "cover-art"):as( + function(cover) + return "background-image: url('" .. (cover or "") .. "');" + end + ), }), Widget.Label({ - label = bind(player, "metadata"):as(function() - return (player.title or "") .. " - " .. (player.artist or "") - end), + label = bind(player, "metadata"):as( + function() + return (player.title or "") + .. " - " + .. (player.artist or "") + end + ), }), }) end @@ -134,50 +137,50 @@ local function Workspaces() return Widget.Box({ class_name = "Workspaces", bind(hypr, "workspaces"):as(function(wss) - table.sort(wss, function(a, b) - return a.id < b.id - end) + table.sort(wss, function(a, b) return a.id < b.id end) return map(wss, function(ws) - return Widget.Button({ - class_name = bind(hypr, "focused-workspace"):as(function(fw) - return fw == ws and "focused" or "" - end), - on_clicked = function() - ws:focus() - end, - label = bind(ws, "id"):as(function(v) - return type(v) == "number" and string.format("%.0f", v) or v - end), - }) + if not (ws.id >= -99 and ws.id <= -2) then -- filter out special workspaces + return Widget.Button({ + class_name = bind(hypr, "focused-workspace"):as( + function(fw) return fw == ws and "focused" or "" end + ), + on_clicked = function() ws:focus() end, + label = bind(ws, "id"):as( + function(v) + return type(v) == "number" + and string.format("%.0f", v) + or v + end + ), + }) + end end) end), }) end local function Time(format) - local time = Variable(""):poll(1000, function() - return GLib.DateTime.new_now_local():format(format) - end) + local time = Variable(""):poll( + 1000, + function() return GLib.DateTime.new_now_local():format(format) end + ) return Widget.Label({ class_name = "Time", - on_destroy = function() - time:drop() - end, + on_destroy = function() time:drop() end, label = time(), }) end return function(gdkmonitor) - local WindowAnchor = astal.require("Astal", "3.0").WindowAnchor + local Anchor = astal.require("Astal").WindowAnchor return Widget.Window({ class_name = "Bar", gdkmonitor = gdkmonitor, - anchor = WindowAnchor.TOP + WindowAnchor.LEFT + WindowAnchor.RIGHT, + anchor = Anchor.TOP + Anchor.LEFT + Anchor.RIGHT, exclusivity = "EXCLUSIVE", - Widget.CenterBox({ Widget.Box({ halign = "START", @@ -189,10 +192,10 @@ return function(gdkmonitor) }), Widget.Box({ halign = "END", + SysTray(), Wifi(), AudioSlider(), BatteryLevel(), - SysTray(), Time("%H:%M - %A %e."), }), }), diff --git a/examples/gtk3/lua/stylua.toml b/examples/gtk3/lua/stylua.toml new file mode 100644 index 0000000..4141934 --- /dev/null +++ b/examples/gtk3/lua/stylua.toml @@ -0,0 +1,4 @@ +indent_type = "Tabs" +indent_width = 4 +column_width = 80 +collapse_simple_statement = "Always" diff --git a/examples/py/.gitignore b/examples/gtk3/py/.gitignore index c18dd8d..c18dd8d 100644 --- a/examples/py/.gitignore +++ b/examples/gtk3/py/.gitignore diff --git a/examples/py/simple-bar/README.md b/examples/gtk3/py/simple-bar/README.md index 48cc27c..48cc27c 100644 --- a/examples/py/simple-bar/README.md +++ b/examples/gtk3/py/simple-bar/README.md diff --git a/examples/py/simple-bar/__init__.py b/examples/gtk3/py/simple-bar/__init__.py index e69de29..e69de29 100644 --- a/examples/py/simple-bar/__init__.py +++ b/examples/gtk3/py/simple-bar/__init__.py diff --git a/examples/py/simple-bar/app.py b/examples/gtk3/py/simple-bar/app.py index 17b6782..d95dc0e 100755 --- a/examples/py/simple-bar/app.py +++ b/examples/gtk3/py/simple-bar/app.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import sys import versions +import subprocess from gi.repository import AstalIO, Astal, Gio from widget.Bar import Bar from pathlib import Path @@ -18,9 +19,8 @@ class App(Astal.Application): def do_activate(self) -> None: self.hold() - AstalIO.Process.execv(["sass", scss, css]) + subprocess.run(["sass", scss, css]) self.apply_css(css, True) - print("hello") for mon in self.get_monitors(): self.add_window(Bar(mon)) @@ -30,7 +30,7 @@ app = App(instance_name=instance_name) if __name__ == "__main__": try: - print(app.acquire_socket()) + app.acquire_socket() app.run(None) except Exception as e: print(AstalIO.send_message(instance_name, "".join(sys.argv[1:]))) diff --git a/examples/js/simple-bar/style.scss b/examples/gtk3/py/simple-bar/style.scss index 1dcf729..f5f771a 100644 --- a/examples/js/simple-bar/style.scss +++ b/examples/gtk3/py/simple-bar/style.scss @@ -13,31 +13,45 @@ window.Bar { font-size: 1.1em; font-weight: bold; - button { - all: unset; - background-color: transparent; + label { + margin: 0 8px; + } + + .Workspaces { + button { + all: unset; + background-color: transparent; - &:hover label { - background-color: color.adjust($fg, $alpha: -0.84); - border-color: color.adjust($accent, $alpha: -0.8); + &:hover label { + background-color: color.adjust($fg, $alpha: -0.84); + border-color: color.adjust($accent, $alpha: -0.8); + } + + &:active label { + background-color: color.adjust($fg, $alpha: -0.8) + } } - &:active label { - background-color: color.adjust($fg, $alpha: -0.8) + label { + transition: 200ms; + padding: 0 8px; + margin: 2px; + border-radius: $radius; + border: 1pt solid transparent; } - } - label { - transition: 200ms; - padding: 0 8px; - margin: 2px; - border-radius: $radius; - border: 1pt solid transparent; + .focused label { + color: $accent; + border-color: $accent; + } } - .Workspaces .focused label { - color: $accent; - border-color: $accent; + .SysTray { + margin-right: 8px; + + button { + padding: 0 4px; + } } .FocusedClient { diff --git a/examples/py/simple-bar/versions.py b/examples/gtk3/py/simple-bar/versions.py index 0e57708..0e57708 100644 --- a/examples/py/simple-bar/versions.py +++ b/examples/gtk3/py/simple-bar/versions.py diff --git a/examples/py/simple-bar/widget/Bar.py b/examples/gtk3/py/simple-bar/widget/Bar.py index 3b09dce..555ab85 100644 --- a/examples/py/simple-bar/widget/Bar.py +++ b/examples/gtk3/py/simple-bar/widget/Bar.py @@ -32,7 +32,8 @@ class Workspaces(Gtk.Box): child.destroy() for ws in hypr.get_workspaces(): - self.add(self.button(ws)) + if not (ws.get_id() >= -99 and ws.get_id() <= -2): # filter out special workspaces + self.add(self.button(ws)) def button(self, ws): hypr = Hyprland.get_default() @@ -108,6 +109,7 @@ class Media(Gtk.Box): class SysTray(Gtk.Box): def __init__(self) -> None: super().__init__() + Astal.widget_set_class_names(self, ["SysTray"]) self.items = {} tray = Tray.get_default() tray.connect("item_added", self.add_item) @@ -118,33 +120,22 @@ class SysTray(Gtk.Box): return item = Tray.get_default().get_item(id) - theme = item.get_icon_theme_path() - - if theme is not None: - from app import app - - app.add_icons(theme) - - menu = item.create_menu() - btn = Astal.Button(visible=True) + btn = Gtk.MenuButton(use_popover=False, visible=True) icon = Astal.Icon(visible=True) - def on_clicked(btn): - if menu: - menu.popup_at_widget(btn, Gdk.Gravity.SOUTH, Gdk.Gravity.NORTH, None) + item.bind_property("tooltip-markup", btn, "tooltip-markup", SYNC) + item.bind_property("gicon", icon, "gicon", SYNC) + item.bind_property("menu-model", btn, "menu-model", SYNC) + btn.insert_action_group("dbusmenu", item.get_action_group()) - def on_destroy(btn): - if menu: - menu.destroy() + def on_action_group(*args): + btn.insert_action_group("dbusmenu", item.get_action_group()) - btn.connect("clicked", on_clicked) - btn.connect("destroy", on_destroy) + item.connect("notify::action-group", on_action_group) - item.bind_property("tooltip-markup", btn, "tooltip-markup", SYNC) - item.bind_property("gicon", icon, "gicon", SYNC) + btn.add(icon) self.add(btn) self.items[id] = btn - self.show_all() def remove_item(self, _: Tray.Tray, id: str): if id in self.items: @@ -156,8 +147,9 @@ class Wifi(Astal.Icon): super().__init__() Astal.widget_set_class_names(self, ["Wifi"]) wifi = Network.get_default().get_wifi() - wifi.bind_property("ssid", self, "tooltip-text", SYNC) - wifi.bind_property("icon-name", self, "icon", SYNC) + if wifi: + wifi.bind_property("ssid", self, "tooltip-text", SYNC) + wifi.bind_property("icon-name", self, "icon", SYNC) class AudioSlider(Gtk.Box): diff --git a/examples/py/simple-bar/widget/__init__.py b/examples/gtk3/py/simple-bar/widget/__init__.py index e69de29..e69de29 100644 --- a/examples/py/simple-bar/widget/__init__.py +++ b/examples/gtk3/py/simple-bar/widget/__init__.py diff --git a/examples/vala/simple-bar/README.md b/examples/gtk3/vala/simple-bar/README.md index 48cc27c..48cc27c 100644 --- a/examples/vala/simple-bar/README.md +++ b/examples/gtk3/vala/simple-bar/README.md diff --git a/examples/vala/simple-bar/app.in.vala b/examples/gtk3/vala/simple-bar/app.in.vala index b04a1fa..b04a1fa 100644 --- a/examples/vala/simple-bar/app.in.vala +++ b/examples/gtk3/vala/simple-bar/app.in.vala diff --git a/examples/vala/simple-bar/flake.nix b/examples/gtk3/vala/simple-bar/flake.nix index d13c649..d13c649 100644 --- a/examples/vala/simple-bar/flake.nix +++ b/examples/gtk3/vala/simple-bar/flake.nix diff --git a/examples/vala/simple-bar/meson.build b/examples/gtk3/vala/simple-bar/meson.build index 10f5dd2..5a0ef4c 100644 --- a/examples/vala/simple-bar/meson.build +++ b/examples/gtk3/vala/simple-bar/meson.build @@ -21,22 +21,15 @@ pkgconfig_deps = [ # needed for GLib.Math deps = pkgconfig_deps + meson.get_compiler('c').find_library('m') -custom_target( - 'style.css', - command: [ - find_program('sass'), - meson.project_source_root() / 'style.scss', - '@OUTPUT@', - ], - output: 'style.css', - install: true, - install_dir: libdir, -) - main = configure_file( input: 'app.in.vala', output: 'app.vala', - configuration: {'STYLE': libdir / 'style.css'}, + configuration: { + 'STYLE': run_command( + find_program('sass'), + meson.project_source_root() / 'style.scss', + ).stdout(), + }, ) sources = files( diff --git a/examples/py/simple-bar/style.scss b/examples/gtk3/vala/simple-bar/style.scss index 1dcf729..f5f771a 100644 --- a/examples/py/simple-bar/style.scss +++ b/examples/gtk3/vala/simple-bar/style.scss @@ -13,31 +13,45 @@ window.Bar { font-size: 1.1em; font-weight: bold; - button { - all: unset; - background-color: transparent; + label { + margin: 0 8px; + } + + .Workspaces { + button { + all: unset; + background-color: transparent; - &:hover label { - background-color: color.adjust($fg, $alpha: -0.84); - border-color: color.adjust($accent, $alpha: -0.8); + &:hover label { + background-color: color.adjust($fg, $alpha: -0.84); + border-color: color.adjust($accent, $alpha: -0.8); + } + + &:active label { + background-color: color.adjust($fg, $alpha: -0.8) + } } - &:active label { - background-color: color.adjust($fg, $alpha: -0.8) + label { + transition: 200ms; + padding: 0 8px; + margin: 2px; + border-radius: $radius; + border: 1pt solid transparent; } - } - label { - transition: 200ms; - padding: 0 8px; - margin: 2px; - border-radius: $radius; - border: 1pt solid transparent; + .focused label { + color: $accent; + border-color: $accent; + } } - .Workspaces .focused label { - color: $accent; - border-color: $accent; + .SysTray { + margin-right: 8px; + + button { + padding: 0 4px; + } } .FocusedClient { diff --git a/examples/vala/simple-bar/widget/Bar.vala b/examples/gtk3/vala/simple-bar/widget/Bar.vala index ba4062c..28b32ef 100644 --- a/examples/vala/simple-bar/widget/Bar.vala +++ b/examples/gtk3/vala/simple-bar/widget/Bar.vala @@ -10,8 +10,12 @@ class Workspaces : Gtk.Box { foreach (var child in get_children()) child.destroy(); - foreach (var ws in hypr.workspaces) - add(button(ws)); + foreach (var ws in hypr.workspaces) { + // filter out special workspaces + if (!(ws.id >= -99 && ws.id <= -2)) { + add(button(ws)); + } + } } Gtk.Button button(AstalHyprland.Workspace ws) { @@ -103,6 +107,7 @@ class SysTray : Gtk.Box { AstalTray.Tray tray = AstalTray.get_default(); public SysTray() { + Astal.widget_set_class_names(this, { "SysTray" }); tray.item_added.connect(add_item); tray.item_removed.connect(remove_item); } @@ -112,26 +117,19 @@ class SysTray : Gtk.Box { return; var item = tray.get_item(id); + var btn = new Gtk.MenuButton() { use_popover = false, visible = true }; + var icon = new Astal.Icon() { visible = true }; - var menu = item.create_menu(); - var btn = new Astal.Button(); - var icon = new Astal.Icon(); - - btn.clicked.connect(() => { - if (menu != null) - menu.popup_at_widget(this, Gdk.Gravity.SOUTH, Gdk.Gravity.NORTH, null); - }); - - btn.destroy.connect(() => { - if (menu != null) - menu.destroy(); + item.bind_property("tooltip-markup", btn, "tooltip-markup", BindingFlags.SYNC_CREATE); + item.bind_property("gicon", icon, "gicon", BindingFlags.SYNC_CREATE); + item.bind_property("menu-model", btn, "menu-model", BindingFlags.SYNC_CREATE); + btn.insert_action_group("dbusmenu", item.action_group); + item.notify["action-group"].connect(() => { + btn.insert_action_group("dbusmenu", item.action_group); }); - item.bind_property("tooltip-markup", btn, "tooltip-markup", BindingFlags.SYNC_CREATE); - item.bind_property("gicon", icon, "g-icon", BindingFlags.SYNC_CREATE); btn.add(icon); add(btn); - btn.show_all(); items.set(id, btn); } @@ -145,9 +143,11 @@ class SysTray : Gtk.Box { class Wifi : Astal.Icon { public Wifi() { Astal.widget_set_class_names(this, {"Wifi"}); - var wifi = AstalNetwork.get_default().wifi; - wifi.bind_property("ssid", this, "tooltip-text", BindingFlags.SYNC_CREATE); - wifi.bind_property("icon-name", this, "icon", BindingFlags.SYNC_CREATE); + var wifi = AstalNetwork.get_default().get_wifi(); + if (wifi != null) { + wifi.bind_property("ssid", this, "tooltip-text", BindingFlags.SYNC_CREATE); + wifi.bind_property("icon-name", this, "icon", BindingFlags.SYNC_CREATE); + } } } @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1729256560, - "narHash": "sha256-/uilDXvCIEs3C9l73JTACm4quuHUsIHcns1c+cHUJwA=", + "lastModified": 1737469691, + "narHash": "sha256-nmKOgAU48S41dTPIXAq0AHZSehWUn6ZPrUKijHAMmIk=", "owner": "nixos", "repo": "nixpkgs", - "rev": "4c2fcb090b1f3e5b47eaa7bd33913b574a11e0a0", + "rev": "9e4d5190a9482a1fb9d18adf0bdb83c6e506eaab", "type": "github" }, "original": { @@ -3,80 +3,49 @@ self, nixpkgs, }: let - inherit (builtins) replaceStrings readFile; - readVer = file: replaceStrings ["\n"] [""] (readFile file); - - system = "x86_64-linux"; # TODO: other architectures - pkgs = nixpkgs.legacyPackages.${system}; - - mkPkg = name: src: inputs: - pkgs.stdenv.mkDerivation { - nativeBuildInputs = with pkgs; [ - wrapGAppsHook - gobject-introspection - meson - pkg-config - ninja - vala - wayland - wayland-scanner - python3 - ]; - propagatedBuildInputs = [pkgs.glib] ++ inputs; - pname = name; - version = readVer "${src}/version"; - src = src; - postUnpack = '' - cp --remove-destination ${./lib/gir.py} $sourceRoot/gir.py - ''; - outputs = ["out" "dev"]; - }; + forAllSystems = nixpkgs.lib.genAttrs ["x86_64-linux" "aarch64-linux"]; in { - devShells.${system} = import ./nix/devshell.nix { - inherit self pkgs; - }; - lib = { - mkLuaPackage = import ./nix/lua.nix { - inherit pkgs; - astal = self; - }; + mkLuaPackage = import ./nix/lua.nix self; }; - packages.${system} = with pkgs; { - docs = import ./docs {inherit self pkgs;}; + packages = forAllSystems (system: let + pkgs = nixpkgs.legacyPackages.${system}; + mkPkg = src: + import src { + inherit self pkgs; + mkAstalPkg = import ./nix/mkAstalPkg.nix pkgs; + }; + in { default = self.packages.${system}.io; + docs = import ./docs {inherit self pkgs;}; - io = mkPkg "astal" ./lib/astal/io []; - astal3 = mkPkg "astal" ./lib/astal/gtk3 [self.packages.${system}.io gtk3 gtk-layer-shell]; - astal4 = mkPkg "astal" ./lib/astal/gtk4 [self.packages.${system}.io gtk4 gtk4-layer-shell]; - apps = mkPkg "astal-apps" ./lib/apps [json-glib]; - auth = mkPkg "astal-auth" ./lib/auth [pam]; - battery = mkPkg "astal-battery" ./lib/battery [json-glib]; - bluetooth = mkPkg "astal-bluetooth" ./lib/bluetooth []; - cava = mkPkg "astal-cava" ./lib/cava [(pkgs.callPackage ./nix/libcava.nix {})]; - greet = mkPkg "astal-greet" ./lib/greet [json-glib]; - hyprland = mkPkg "astal-hyprland" ./lib/hyprland [json-glib]; - mpris = mkPkg "astal-mpris" ./lib/mpris [gvfs json-glib]; - network = mkPkg "astal-network" ./lib/network [networkmanager]; - notifd = mkPkg "astal-notifd" ./lib/notifd [json-glib gdk-pixbuf]; - powerprofiles = mkPkg "astal-power-profiles" ./lib/powerprofiles [json-glib]; - river = mkPkg "astal-river" ./lib/river [json-glib]; - tray = mkPkg "astal-tray" ./lib/tray [gtk3 gdk-pixbuf libdbusmenu-gtk3 json-glib]; - wireplumber = mkPkg "astal-wireplumber" ./lib/wireplumber [wireplumber]; + io = mkPkg ./lib/astal/io; + astal3 = mkPkg ./lib/astal/gtk3; + astal4 = mkPkg ./lib/astal/gtk4; + apps = mkPkg ./lib/apps; + auth = mkPkg ./lib/auth; + battery = mkPkg ./lib/battery; + bluetooth = mkPkg ./lib/bluetooth; + cava = mkPkg ./lib/cava; + greet = mkPkg ./lib/greet; + hyprland = mkPkg ./lib/hyprland; + mpris = mkPkg ./lib/mpris; + network = mkPkg ./lib/network; + notifd = mkPkg ./lib/notifd; + powerprofiles = mkPkg ./lib/powerprofiles; + river = mkPkg ./lib/river; + tray = mkPkg ./lib/tray; + wireplumber = mkPkg ./lib/wireplumber; - gjs = pkgs.stdenvNoCC.mkDerivation { - src = ./lang/gjs; - name = "astal-gjs"; - nativeBuildInputs = [ - meson - ninja - pkg-config - self.packages.${system}.io - self.packages.${system}.astal3 - ]; - }; - }; + gjs = import ./lang/gjs {inherit self pkgs;}; + }); + + devShells = forAllSystems (system: + import ./nix/devshell.nix { + inherit self; + pkgs = nixpkgs.legacyPackages.${system}; + }); }; inputs = { diff --git a/lang/gjs/default.nix b/lang/gjs/default.nix new file mode 100644 index 0000000..e4ffaf0 --- /dev/null +++ b/lang/gjs/default.nix @@ -0,0 +1,16 @@ +{ + pkgs, + self, + ... +}: +pkgs.stdenvNoCC.mkDerivation { + src = ./.; + name = "astal-gjs"; + nativeBuildInputs = [ + pkgs.meson + pkgs.ninja + pkgs.pkg-config + self.packages.${pkgs.system}.io + self.packages.${pkgs.system}.astal3 + ]; +} diff --git a/lang/gjs/eslint.config.mjs b/lang/gjs/eslint.config.mjs index 05e49ee..5e32355 100644 --- a/lang/gjs/eslint.config.mjs +++ b/lang/gjs/eslint.config.mjs @@ -15,5 +15,6 @@ export default tseslint.config({ rules: { "@typescript-eslint/no-explicit-any": "off", "@stylistic/new-parens": "off", + "@stylistic/brace-style": ["error", "1tbs", { allowSingleLine: true }], }, }) diff --git a/lang/gjs/meson.build b/lang/gjs/meson.build index 51496dc..48d13a2 100644 --- a/lang/gjs/meson.build +++ b/lang/gjs/meson.build @@ -3,19 +3,27 @@ project('astal-gjs') dest = get_option('prefix') / get_option('datadir') / 'astal' / 'gjs' dependency('astal-io-0.1') -dependency('astal-3.0') + +gtk3 = dependency('astal-3.0', required: false) +gtk4 = dependency('astal-4-4.0', required: false) + +if (not gtk3.found() and not gtk4.found()) + error('Neither astal-3.0 nor astal-4.0 was found.') +endif install_data( [ + 'src/_app.ts', + 'src/_astal.ts', 'src/binding.ts', 'src/file.ts', 'src/gobject.ts', 'src/index.ts', + 'src/overrides.ts', 'src/process.ts', 'src/time.ts', 'src/variable.ts', - 'src/overrides.ts', - 'src/_app.ts', + 'src/package.json', ], install_dir: dest, ) diff --git a/lang/gjs/package.json b/lang/gjs/package.json index 43a7702..e3b7761 100644 --- a/lang/gjs/package.json +++ b/lang/gjs/package.json @@ -18,6 +18,12 @@ ".": "./index.ts", "./gtk3": "./src/gtk3/index.ts", "./gtk4": "./src/gtk4/index.ts", + "./gtk3/app": "./src/gtk3/app.ts", + "./gtk4/app": "./src/gtk4/app.ts", + "./gtk3/widget": "./src/gtk3/widget.ts", + "./gtk4/widget": "./src/gtk4/widget.ts", + "./gtk3/jsx-runtime": "./src/gtk3/jsx-runtime.ts", + "./gtk4/jsx-runtime": "./src/gtk4/jsx-runtime.ts", "./binding": "./src/binding.ts", "./file": "./src/file.ts", "./gobject": "./src/gobject.ts", @@ -42,6 +48,6 @@ }, "scripts": { "lint": "eslint . --fix", - "types": "ts-for-gir generate -o @girs" + "types": "ts-for-gir generate -o @girs --ignoreVersionConflicts" } } diff --git a/lang/gjs/src/_app.ts b/lang/gjs/src/_app.ts index 3dadd04..46497c1 100644 --- a/lang/gjs/src/_app.ts +++ b/lang/gjs/src/_app.ts @@ -53,8 +53,7 @@ export function mkApp(App: App3 | App4) { ${body.includes(";") ? body : `return ${body};`} })`) fn()().then(res).catch(rej) - } - catch (error) { + } catch (error) { rej(error) } }) @@ -69,8 +68,7 @@ export function mkApp(App: App3 | App4) { IO.write_sock_finish(res), ) }) - } - else { + } else { super.vfunc_request(msg, conn) } } @@ -102,8 +100,7 @@ export function mkApp(App: App3 | App4) { try { app.acquire_socket() - } - catch (error) { + } catch (error) { return client(msg => IO.send_message(app.instanceName, msg)!, ...programArgs) } diff --git a/lang/gjs/src/_astal.ts b/lang/gjs/src/_astal.ts new file mode 100644 index 0000000..d163687 --- /dev/null +++ b/lang/gjs/src/_astal.ts @@ -0,0 +1,193 @@ +import Variable from "./variable.js" +import { execAsync } from "./process.js" +import Binding, { Connectable, kebabify, snakeify, Subscribable } from "./binding.js" + +export const noImplicitDestroy = Symbol("no no implicit destroy") +export const setChildren = Symbol("children setter method") + +export function mergeBindings(array: any[]) { + function getValues(...args: any[]) { + let i = 0 + return array.map(value => value instanceof Binding + ? args[i++] + : value, + ) + } + + const bindings = array.filter(i => i instanceof Binding) + + if (bindings.length === 0) + return array + + if (bindings.length === 1) + return bindings[0].as(getValues) + + return Variable.derive(bindings, getValues)() +} + +export function setProp(obj: any, prop: string, value: any) { + try { + const setter = `set_${snakeify(prop)}` + if (typeof obj[setter] === "function") + return obj[setter](value) + + return (obj[prop] = value) + } catch (error) { + console.error(`could not set property "${prop}" on ${obj}:`, error) + } +} + +export type BindableProps<T> = { + [K in keyof T]: Binding<T[K]> | T[K]; +} + +export function hook<Widget extends Connectable>( + widget: Widget, + object: Connectable | Subscribable, + signalOrCallback: string | ((self: Widget, ...args: any[]) => void), + callback?: (self: Widget, ...args: any[]) => void, +) { + if (typeof object.connect === "function" && callback) { + const id = object.connect(signalOrCallback, (_: any, ...args: unknown[]) => { + return callback(widget, ...args) + }) + widget.connect("destroy", () => { + (object.disconnect as Connectable["disconnect"])(id) + }) + } else if (typeof object.subscribe === "function" && typeof signalOrCallback === "function") { + const unsub = object.subscribe((...args: unknown[]) => { + signalOrCallback(widget, ...args) + }) + widget.connect("destroy", unsub) + } +} + +export function construct<Widget extends Connectable & { [setChildren]: (children: any[]) => void }>(widget: Widget, config: any) { + // eslint-disable-next-line prefer-const + let { setup, child, children = [], ...props } = config + + if (children instanceof Binding) { + children = [children] + } + + if (child) { + children.unshift(child) + } + + // remove undefined values + for (const [key, value] of Object.entries(props)) { + if (value === undefined) { + delete props[key] + } + } + + // collect bindings + const bindings: Array<[string, Binding<any>]> = Object + .keys(props) + .reduce((acc: any, prop) => { + if (props[prop] instanceof Binding) { + const binding = props[prop] + delete props[prop] + return [...acc, [prop, binding]] + } + return acc + }, []) + + // collect signal handlers + const onHandlers: Array<[string, string | (() => unknown)]> = Object + .keys(props) + .reduce((acc: any, key) => { + if (key.startsWith("on")) { + const sig = kebabify(key).split("-").slice(1).join("-") + const handler = props[key] + delete props[key] + return [...acc, [sig, handler]] + } + return acc + }, []) + + // set children + const mergedChildren = mergeBindings(children.flat(Infinity)) + if (mergedChildren instanceof Binding) { + widget[setChildren](mergedChildren.get()) + widget.connect("destroy", mergedChildren.subscribe((v) => { + widget[setChildren](v) + })) + } else { + if (mergedChildren.length > 0) { + widget[setChildren](mergedChildren) + } + } + + // setup signal handlers + for (const [signal, callback] of onHandlers) { + const sig = signal.startsWith("notify") + ? signal.replace("-", "::") + : signal + + if (typeof callback === "function") { + widget.connect(sig, callback) + } else { + widget.connect(sig, () => execAsync(callback) + .then(print).catch(console.error)) + } + } + + // setup bindings handlers + for (const [prop, binding] of bindings) { + if (prop === "child" || prop === "children") { + widget.connect("destroy", binding.subscribe((v: any) => { + widget[setChildren](v) + })) + } + widget.connect("destroy", binding.subscribe((v: any) => { + setProp(widget, prop, v) + })) + setProp(widget, prop, binding.get()) + } + + // filter undefined values + for (const [key, value] of Object.entries(props)) { + if (value === undefined) { + delete props[key] + } + } + + Object.assign(widget, props) + setup?.(widget) + return widget +} + +function isArrowFunction(func: any): func is (args: any) => any { + return !Object.hasOwn(func, "prototype") +} + +export function jsx( + ctors: Record<string, { new(props: any): any } | ((props: any) => any)>, + ctor: string | ((props: any) => any) | { new(props: any): any }, + { children, ...props }: any, +) { + children ??= [] + + if (!Array.isArray(children)) + children = [children] + + children = children.filter(Boolean) + + if (children.length === 1) + props.child = children[0] + else if (children.length > 1) + props.children = children + + if (typeof ctor === "string") { + if (isArrowFunction(ctors[ctor])) + return ctors[ctor](props) + + return new ctors[ctor](props) + } + + if (isArrowFunction(ctor)) + return ctor(props) + + return new ctor(props) +} diff --git a/lang/gjs/src/binding.ts b/lang/gjs/src/binding.ts index 95d905f..616c53b 100644 --- a/lang/gjs/src/binding.ts +++ b/lang/gjs/src/binding.ts @@ -20,7 +20,7 @@ export interface Connectable { [key: string]: any } -export default class Binding<Value> { +export class Binding<Value> { private transformFn = (v: any) => v #emitter: Subscribable<Value> | Connectable @@ -72,8 +72,7 @@ export default class Binding<Value> { return this.#emitter.subscribe(() => { callback(this.get()) }) - } - else if (typeof this.#emitter.connect === "function") { + } else if (typeof this.#emitter.connect === "function") { const signal = `notify::${this.#prop}` const id = this.#emitter.connect(signal, () => { callback(this.get()) @@ -87,3 +86,4 @@ export default class Binding<Value> { } export const { bind } = Binding +export default Binding diff --git a/lang/gjs/src/file.ts b/lang/gjs/src/file.ts index 6ad8be3..4220d9d 100644 --- a/lang/gjs/src/file.ts +++ b/lang/gjs/src/file.ts @@ -12,8 +12,7 @@ export function readFileAsync(path: string): Promise<string> { Astal.read_file_async(path, (_, res) => { try { resolve(Astal.read_file_finish(res) || "") - } - catch (error) { + } catch (error) { reject(error) } }) @@ -29,8 +28,7 @@ export function writeFileAsync(path: string, content: string): Promise<void> { Astal.write_file_async(path, content, (_, res) => { try { resolve(Astal.write_file_finish(res)) - } - catch (error) { + } catch (error) { reject(error) } }) diff --git a/lang/gjs/src/gobject.ts b/lang/gjs/src/gobject.ts index b744cfb..648c1d7 100644 --- a/lang/gjs/src/gobject.ts +++ b/lang/gjs/src/gobject.ts @@ -90,9 +90,7 @@ export function property(declaration: PropertyDeclaration = Object) { }) target.constructor[meta].Properties[kebabify(prop)] = pspec(name, ParamFlags.READWRITE, declaration) - } - - else { + } else { let flags = 0 if (desc.get) flags |= ParamFlags.READABLE if (desc.set) flags |= ParamFlags.WRITABLE @@ -124,8 +122,7 @@ export function signal( target.constructor[meta].Signals[name] = { param_types: arr, } - } - else { + } else { target.constructor[meta].Signals[name] = declaration || { param_types: [], } @@ -137,8 +134,7 @@ export function signal( this.emit(name, ...args) }, }) - } - else { + } else { const og: ((...args: any[]) => void) = desc.value desc.value = function (...args: any[]) { // @ts-expect-error not typed @@ -146,7 +142,7 @@ export function signal( } Object.defineProperty(target, `on_${name.replace("-", "_")}`, { value: function (...args: any[]) { - return og(...args) + return og.apply(this, args) }, }) } @@ -178,7 +174,7 @@ function defaultValue(declaration: PropertyDeclaration) { switch (declaration) { case String: - return "default-string" + return "" case Number: return 0 case Boolean: diff --git a/lang/gjs/src/gtk3/astalify.ts b/lang/gjs/src/gtk3/astalify.ts index 6973805..6a3bec2 100644 --- a/lang/gjs/src/gtk3/astalify.ts +++ b/lang/gjs/src/gtk3/astalify.ts @@ -1,45 +1,12 @@ +import { hook, noImplicitDestroy, setChildren, mergeBindings, type BindableProps, construct } from "../_astal.js" import Astal from "gi://Astal?version=3.0" import Gtk from "gi://Gtk?version=3.0" import Gdk from "gi://Gdk?version=3.0" import GObject from "gi://GObject" -import { execAsync } from "../process.js" -import Variable from "../variable.js" -import Binding, { kebabify, snakeify, type Connectable, type Subscribable } from "../binding.js" +import Gio from "gi://Gio?version=2.0" +import Binding, { type Connectable, type Subscribable } from "../binding.js" -export function mergeBindings(array: any[]) { - function getValues(...args: any[]) { - let i = 0 - return array.map(value => value instanceof Binding - ? args[i++] - : value, - ) - } - - const bindings = array.filter(i => i instanceof Binding) - - if (bindings.length === 0) - return array - - if (bindings.length === 1) - return bindings[0].as(getValues) - - return Variable.derive(bindings, getValues)() -} - -function setProp(obj: any, prop: string, value: any) { - try { - // the setter method has to be used because - // array like properties are not bound correctly as props - const setter = `set_${snakeify(prop)}` - if (typeof obj[setter] === "function") - return obj[setter](value) - - return (obj[prop] = value) - } - catch (error) { - console.error(`could not set property "${prop}" on ${obj}:`, error) - } -} +export { BindableProps, mergeBindings } export default function astalify< C extends { new(...args: any[]): Gtk.Widget }, @@ -65,60 +32,47 @@ export default function astalify< get_click_through(): boolean { return this.clickThrough } set_click_through(clickThrough: boolean) { this.clickThrough = clickThrough } - declare private __no_implicit_destroy: boolean - get noImplicitDestroy(): boolean { return this.__no_implicit_destroy } - set noImplicitDestroy(value: boolean) { this.__no_implicit_destroy = value } + declare private [noImplicitDestroy]: boolean + get noImplicitDestroy(): boolean { return this[noImplicitDestroy] } + set noImplicitDestroy(value: boolean) { this[noImplicitDestroy] = value } + + set actionGroup([prefix, group]: ActionGroup) { this.insert_action_group(prefix, group) } + set_action_group(actionGroup: ActionGroup) { this.actionGroup = actionGroup } + + protected getChildren(): Array<Gtk.Widget> { + if (this instanceof Gtk.Bin) { + return this.get_child() ? [this.get_child()!] : [] + } else if (this instanceof Gtk.Container) { + return this.get_children() + } + return [] + } - _setChildren(children: Gtk.Widget[]) { + protected setChildren(children: any[]) { children = children.flat(Infinity).map(ch => ch instanceof Gtk.Widget ? ch : new Gtk.Label({ visible: true, label: String(ch) })) - // remove - if (this instanceof Gtk.Bin) { - const ch = this.get_child() - if (ch) - this.remove(ch) - if (ch && !children.includes(ch) && !this.noImplicitDestroy) - ch?.destroy() + if (this instanceof Gtk.Container) { + for (const ch of children) + this.add(ch) + } else { + throw Error(`can not add children to ${this.constructor.name}`) } - else if (this instanceof Gtk.Container) { - for (const ch of this.get_children()) { + } + + [setChildren](children: any[]) { + // remove + if (this instanceof Gtk.Container) { + for (const ch of this.getChildren()) { this.remove(ch) if (!children.includes(ch) && !this.noImplicitDestroy) ch?.destroy() } } - // TODO: add more container types - if (this instanceof Astal.Box) { - this.set_children(children) - } - - else if (this instanceof Astal.Stack) { - this.set_children(children) - } - - else if (this instanceof Astal.CenterBox) { - this.startWidget = children[0] - this.centerWidget = children[1] - this.endWidget = children[2] - } - - else if (this instanceof Astal.Overlay) { - const [child, ...overlays] = children - this.set_child(child) - this.set_overlays(overlays) - } - - else if (this instanceof Gtk.Container) { - for (const ch of children) - this.add(ch) - } - - else { - throw Error(`can not add children to ${this.constructor.name}, it is not a container widget`) - } + // append + this.setChildren(children) } toggleClassName(cn: string, cond = true) { @@ -139,96 +93,15 @@ export default function astalify< signalOrCallback: string | ((self: this, ...args: any[]) => void), callback?: (self: this, ...args: any[]) => void, ) { - if (typeof object.connect === "function" && callback) { - const id = object.connect(signalOrCallback, (_: any, ...args: unknown[]) => { - callback(this, ...args) - }) - this.connect("destroy", () => { - (object.disconnect as Connectable["disconnect"])(id) - }) - } - - else if (typeof object.subscribe === "function" && typeof signalOrCallback === "function") { - const unsub = object.subscribe((...args: unknown[]) => { - signalOrCallback(this, ...args) - }) - this.connect("destroy", unsub) - } - + hook(this, object, signalOrCallback, callback) return this } constructor(...params: any[]) { super() - const [config] = params - - const { setup, child, children = [], ...props } = config + const props = params[0] || {} props.visible ??= true - - if (child) - children.unshift(child) - - // collect bindings - const bindings = Object.keys(props).reduce((acc: any, prop) => { - if (props[prop] instanceof Binding) { - const binding = props[prop] - delete props[prop] - return [...acc, [prop, binding]] - } - return acc - }, []) - - // collect signal handlers - const onHandlers = Object.keys(props).reduce((acc: any, key) => { - if (key.startsWith("on")) { - const sig = kebabify(key).split("-").slice(1).join("-") - const handler = props[key] - delete props[key] - return [...acc, [sig, handler]] - } - return acc - }, []) - - // set children - const mergedChildren = mergeBindings(children.flat(Infinity)) - if (mergedChildren instanceof Binding) { - this._setChildren(mergedChildren.get()) - this.connect("destroy", mergedChildren.subscribe((v) => { - this._setChildren(v) - })) - } - else { - if (mergedChildren.length > 0) { - this._setChildren(mergedChildren) - } - } - - // setup signal handlers - for (const [signal, callback] of onHandlers) { - if (typeof callback === "function") { - this.connect(signal, callback) - } - else { - this.connect(signal, () => execAsync(callback) - .then(print).catch(console.error)) - } - } - - // setup bindings handlers - for (const [prop, binding] of bindings) { - if (prop === "child" || prop === "children") { - this.connect("destroy", binding.subscribe((v: any) => { - this._setChildren(v) - })) - } - this.connect("destroy", binding.subscribe((v: any) => { - setProp(this, prop, v) - })) - setProp(this, prop, binding.get()) - } - - Object.assign(this, props) - setup?.(this) + construct(this, props) } } @@ -256,15 +129,13 @@ export default function astalify< return Widget } -export type BindableProps<T> = { - [K in keyof T]: Binding<T[K]> | T[K]; -} - type SigHandler< W extends InstanceType<typeof Gtk.Widget>, Args extends Array<unknown>, > = ((self: W, ...args: Args) => unknown) | string | string[] +export type BindableChild = Gtk.Widget | Binding<Gtk.Widget> + export type ConstructProps< Self extends InstanceType<typeof Gtk.Widget>, Props extends Gtk.Widget.ConstructorProps, @@ -274,23 +145,22 @@ export type ConstructProps< [S in keyof Signals]: SigHandler<Self, Signals[S]> }> & Partial<{ [Key in `on${string}`]: SigHandler<Self, any[]> -}> & BindableProps<Partial<Props> & { +}> & BindableProps<Partial<Props & { className?: string css?: string cursor?: string clickThrough?: boolean -}> & { - onDestroy?: (self: Self) => unknown - onDraw?: (self: Self) => unknown - onKeyPressEvent?: (self: Self, event: Gdk.Event) => unknown - onKeyReleaseEvent?: (self: Self, event: Gdk.Event) => unknown - onButtonPressEvent?: (self: Self, event: Gdk.Event) => unknown - onButtonReleaseEvent?: (self: Self, event: Gdk.Event) => unknown - onRealize?: (self: Self) => unknown - setup?: (self: Self) => void -} - -export type BindableChild = Gtk.Widget | Binding<Gtk.Widget> + actionGroup?: ActionGroup +}>> & Partial<{ + onDestroy: (self: Self) => unknown + onDraw: (self: Self) => unknown + onKeyPressEvent: (self: Self, event: Gdk.Event) => unknown + onKeyReleaseEvent: (self: Self, event: Gdk.Event) => unknown + onButtonPressEvent: (self: Self, event: Gdk.Event) => unknown + onButtonReleaseEvent: (self: Self, event: Gdk.Event) => unknown + onRealize: (self: Self) => unknown + setup: (self: Self) => void +}> type Cursor = | "default" @@ -327,3 +197,5 @@ type Cursor = | "nwse-resize" | "zoom-in" | "zoom-out" + +type ActionGroup = [prefix: string, actionGroup: Gio.ActionGroup] diff --git a/lang/gjs/src/gtk3/index.ts b/lang/gjs/src/gtk3/index.ts index ff641af..39a1ae7 100644 --- a/lang/gjs/src/gtk3/index.ts +++ b/lang/gjs/src/gtk3/index.ts @@ -7,3 +7,4 @@ export { Astal, Gtk, Gdk } export { default as App } from "./app.js" export { astalify, ConstructProps, BindableProps } export * as Widget from "./widget.js" +export { hook } from "../_astal" diff --git a/lang/gjs/src/gtk3/jsx-runtime.ts b/lang/gjs/src/gtk3/jsx-runtime.ts index 9da4bb6..ee720af 100644 --- a/lang/gjs/src/gtk3/jsx-runtime.ts +++ b/lang/gjs/src/gtk3/jsx-runtime.ts @@ -1,11 +1,8 @@ import Gtk from "gi://Gtk?version=3.0" -import { mergeBindings, type BindableChild } from "./astalify.js" +import { type BindableChild } from "./astalify.js" +import { mergeBindings, jsx as _jsx } from "../_astal.js" import * as Widget from "./widget.js" -function isArrowFunction(func: any): func is (args: any) => any { - return !Object.hasOwn(func, "prototype") -} - export function Fragment({ children = [], child }: { child?: BindableChild children?: Array<BindableChild> @@ -16,29 +13,9 @@ export function Fragment({ children = [], child }: { export function jsx( ctor: keyof typeof ctors | typeof Gtk.Widget, - { children, ...props }: any, + props: any, ) { - children ??= [] - - if (!Array.isArray(children)) - children = [children] - - children = children.filter(Boolean) - - if (children.length === 1) - props.child = children[0] - else if (children.length > 1) - props.children = children - - if (typeof ctor === "string") { - return new ctors[ctor](props) - } - - if (isArrowFunction(ctor)) - return ctor(props) - - // @ts-expect-error can be class or function - return new ctor(props) + return _jsx(ctors, ctor as any, props) } const ctors = { @@ -55,6 +32,7 @@ const ctors = { label: Widget.Label, levelbar: Widget.LevelBar, // TODO: listbox + menubutton: Widget.MenuButton, overlay: Widget.Overlay, revealer: Widget.Revealer, scrollable: Widget.Scrollable, @@ -83,6 +61,7 @@ declare global { label: Widget.LabelProps levelbar: Widget.LevelBarProps // TODO: listbox + menubutton: Widget.MenuButtonProps overlay: Widget.OverlayProps revealer: Widget.RevealerProps scrollable: Widget.ScrollableProps diff --git a/lang/gjs/src/gtk3/widget.ts b/lang/gjs/src/gtk3/widget.ts index b4e8497..16bcbbd 100644 --- a/lang/gjs/src/gtk3/widget.ts +++ b/lang/gjs/src/gtk3/widget.ts @@ -4,6 +4,12 @@ import Gtk from "gi://Gtk?version=3.0" import GObject from "gi://GObject" import astalify, { type ConstructProps, type BindableChild } from "./astalify.js" +function filter(children: any[]) { + return children.flat(Infinity).map(ch => ch instanceof Gtk.Widget + ? ch + : new Gtk.Label({ visible: true, label: String(ch) })) +} + // Box Object.defineProperty(Astal.Box.prototype, "children", { get() { return this.get_children() }, @@ -14,6 +20,7 @@ export type BoxProps = ConstructProps<Box, Astal.Box.ConstructorProps> export class Box extends astalify(Astal.Box) { static { GObject.registerClass({ GTypeName: "Box" }, this) } constructor(props?: BoxProps, ...children: Array<BindableChild>) { super({ children, ...props } as any) } + protected setChildren(children: any[]): void { this.set_children(filter(children)) } } // Button @@ -35,6 +42,12 @@ export type CenterBoxProps = ConstructProps<CenterBox, Astal.CenterBox.Construct export class CenterBox extends astalify(Astal.CenterBox) { static { GObject.registerClass({ GTypeName: "CenterBox" }, this) } constructor(props?: CenterBoxProps, ...children: Array<BindableChild>) { super({ children, ...props } as any) } + protected setChildren(children: any[]): void { + const ch = filter(children) + this.startWidget = ch[0] || new Gtk.Box + this.centerWidget = ch[1] || new Gtk.Box + this.endWidget = ch[2] || new Gtk.Box + } } // CircularProgress @@ -91,6 +104,7 @@ export type LabelProps = ConstructProps<Label, Astal.Label.ConstructorProps> export class Label extends astalify(Astal.Label) { static { GObject.registerClass({ GTypeName: "Label" }, this) } constructor(props?: LabelProps) { super(props as any) } + protected setChildren(children: any[]): void { this.label = String(children) } } // LevelBar @@ -102,6 +116,13 @@ export class LevelBar extends astalify(Astal.LevelBar) { // TODO: ListBox +// MenuButton +export type MenuButtonProps = ConstructProps<MenuButton, Gtk.MenuButton.ConstructorProps> +export class MenuButton extends astalify(Gtk.MenuButton) { + static { GObject.registerClass({ GTypeName: "MenuButton" }, this) } + constructor(props?: MenuButtonProps, child?: BindableChild) { super({ child, ...props } as any) } +} + // Overlay Object.defineProperty(Astal.Overlay.prototype, "overlays", { get() { return this.get_overlays() }, @@ -112,6 +133,11 @@ export type OverlayProps = ConstructProps<Overlay, Astal.Overlay.ConstructorProp export class Overlay extends astalify(Astal.Overlay) { static { GObject.registerClass({ GTypeName: "Overlay" }, this) } constructor(props?: OverlayProps, ...children: Array<BindableChild>) { super({ children, ...props } as any) } + protected setChildren(children: any[]): void { + const [child, ...overlays] = filter(children) + this.set_child(child) + this.set_overlays(overlays) + } } // Revealer @@ -142,6 +168,7 @@ export type StackProps = ConstructProps<Stack, Astal.Stack.ConstructorProps> export class Stack extends astalify(Astal.Stack) { static { GObject.registerClass({ GTypeName: "Stack" }, this) } constructor(props?: StackProps, ...children: Array<BindableChild>) { super({ children, ...props } as any) } + protected setChildren(children: any[]): void { this.set_children(filter(children)) } } // Switch diff --git a/lang/gjs/src/gtk4/app.ts b/lang/gjs/src/gtk4/app.ts index 1c51772..761bab0 100644 --- a/lang/gjs/src/gtk4/app.ts +++ b/lang/gjs/src/gtk4/app.ts @@ -1,7 +1,18 @@ +import GLib from "gi://GLib?version=2.0" import Gtk from "gi://Gtk?version=4.0" import Astal from "gi://Astal?version=4.0" import { mkApp } from "../_app" Gtk.init() +// stop this from leaking into subprocesses +// and gio launch invocations +GLib.unsetenv("LD_PRELOAD") + +// users might want to use Adwaita in which case it has to be initialized +// it might be common pitfall to forget it because `App` is not `Adw.Application` +await import("gi://Adw?version=1") + .then(({ default: Adw }) => Adw.init()) + .catch(() => void 0) + export default mkApp(Astal.Application) diff --git a/lang/gjs/src/gtk4/astalify.ts b/lang/gjs/src/gtk4/astalify.ts index 6c8ea4d..19d2418 100644 --- a/lang/gjs/src/gtk4/astalify.ts +++ b/lang/gjs/src/gtk4/astalify.ts @@ -1 +1,227 @@ -// TODO: +import { noImplicitDestroy, setChildren, type BindableProps, construct } from "../_astal.js" +import Gtk from "gi://Gtk?version=4.0" +import Gdk from "gi://Gdk?version=4.0" +import Binding from "../binding.js" + +export const type = Symbol("child type") +const dummyBulder = new Gtk.Builder + +function _getChildren(widget: Gtk.Widget): Array<Gtk.Widget> { + if ("get_child" in widget && typeof widget.get_child == "function") { + return widget.get_child() ? [widget.get_child()] : [] + } + + const children: Array<Gtk.Widget> = [] + let ch = widget.get_first_child() + while (ch !== null) { + children.push(ch) + ch = ch.get_next_sibling() + } + return children +} + +function _setChildren(widget: Gtk.Widget, children: any[]) { + children = children.flat(Infinity).map(ch => ch instanceof Gtk.Widget + ? ch + : new Gtk.Label({ visible: true, label: String(ch) })) + + for (const child of children) { + widget.vfunc_add_child( + dummyBulder, + child, + type in widget ? widget[type] as string : null, + ) + } +} + +type Config<T extends Gtk.Widget> = { + setChildren(widget: T, children: any[]): void + getChildren(widget: T): Array<Gtk.Widget> +} + +export default function astalify< + Widget extends Gtk.Widget, + Props extends Gtk.Widget.ConstructorProps = Gtk.Widget.ConstructorProps, + Signals extends Record<`on${string}`, Array<unknown>> = Record<`on${string}`, any[]>, +>(cls: { new(...args: any[]): Widget }, config: Partial<Config<Widget>> = {}) { + Object.assign(cls.prototype, { + [setChildren](children: any[]) { + const w = this as unknown as Widget + for (const child of (config.getChildren?.(w) || _getChildren(w))) { + if (child instanceof Gtk.Widget) { + child.unparent() + if (!children.includes(child) && noImplicitDestroy in this) + child.run_dispose() + } + } + + if (config.setChildren) { + config.setChildren(w, children) + } else { + _setChildren(w, children) + } + }, + }) + + return { + [cls.name]: ( + props: ConstructProps<Widget, Props, Signals> = {}, + ...children: any[] + ): Widget => { + const widget = new cls("cssName" in props ? { cssName: props.cssName } : {}) + + if ("cssName" in props) { + delete props.cssName + } + + if (props.noImplicitDestroy) { + Object.assign(widget, { [noImplicitDestroy]: true }) + delete props.noImplicitDestroy + } + + if (props.type) { + Object.assign(widget, { [type]: props.type }) + delete props.type + } + + if (children.length > 0) { + Object.assign(props, { children }) + } + + return construct(widget as any, setupControllers(widget, props as any)) + }, + }[cls.name] +} + +type SigHandler< + W extends InstanceType<typeof Gtk.Widget>, + Args extends Array<unknown>, +> = ((self: W, ...args: Args) => unknown) | string | string[] + +export { BindableProps } +export type BindableChild = Gtk.Widget | Binding<Gtk.Widget> + +export type ConstructProps< + Self extends InstanceType<typeof Gtk.Widget>, + Props extends Gtk.Widget.ConstructorProps, + Signals extends Record<`on${string}`, Array<unknown>> = Record<`on${string}`, any[]>, +> = Partial<{ + // @ts-expect-error can't assign to unknown, but it works as expected though + [S in keyof Signals]: SigHandler<Self, Signals[S]> +}> & Partial<{ + [Key in `on${string}`]: SigHandler<Self, any[]> +}> & Partial<BindableProps<Omit<Props, "cssName" | "css_name">>> & { + noImplicitDestroy?: true + type?: string + cssName?: string +} & EventController<Self> & { + onDestroy?: (self: Self) => unknown + setup?: (self: Self) => void +} + +type EventController<Self extends Gtk.Widget> = { + onFocusEnter?: (self: Self) => void + onFocusLeave?: (self: Self) => void + + onKeyPressed?: (self: Self, keyval: number, keycode: number, state: Gdk.ModifierType) => void + onKeyReleased?: (self: Self, keyval: number, keycode: number, state: Gdk.ModifierType) => void + onKeyModifier?: (self: Self, state: Gdk.ModifierType) => void + + onLegacy?: (self: Self, event: Gdk.Event) => void + onButtonPressed?: (self: Self, state: Gdk.ButtonEvent) => void + onButtonReleased?: (self: Self, state: Gdk.ButtonEvent) => void + + onHoverEnter?: (self: Self, x: number, y: number) => void + onHoverLeave?: (self: Self) => void + onMotion?: (self: Self, x: number, y: number) => void + + onScroll?: (self: Self, dx: number, dy: number) => void + onScrollDecelerate?: (self: Self, vel_x: number, vel_y: number) => void +} + +function setupControllers<T>(widget: Gtk.Widget, { + onFocusEnter, + onFocusLeave, + onKeyPressed, + onKeyReleased, + onKeyModifier, + onLegacy, + onButtonPressed, + onButtonReleased, + onHoverEnter, + onHoverLeave, + onMotion, + onScroll, + onScrollDecelerate, + ...props +}: EventController<Gtk.Widget> & T) { + if (onFocusEnter || onFocusLeave) { + const focus = new Gtk.EventControllerFocus + widget.add_controller(focus) + + if (onFocusEnter) + focus.connect("enter", () => onFocusEnter(widget)) + + if (onFocusLeave) + focus.connect("leave", () => onFocusLeave(widget)) + } + + if (onKeyPressed || onKeyReleased || onKeyModifier) { + const key = new Gtk.EventControllerKey + widget.add_controller(key) + + if (onKeyPressed) + key.connect("key-pressed", (_, val, code, state) => onKeyPressed(widget, val, code, state)) + + if (onKeyReleased) + key.connect("key-released", (_, val, code, state) => onKeyReleased(widget, val, code, state)) + + if (onKeyModifier) + key.connect("modifiers", (_, state) => onKeyModifier(widget, state)) + } + + if (onLegacy || onButtonPressed || onButtonReleased) { + const legacy = new Gtk.EventControllerLegacy + widget.add_controller(legacy) + + legacy.connect("event", (_, event) => { + if (event.get_event_type() === Gdk.EventType.BUTTON_PRESS) { + onButtonPressed?.(widget, event as Gdk.ButtonEvent) + } + + if (event.get_event_type() === Gdk.EventType.BUTTON_RELEASE) { + onButtonReleased?.(widget, event as Gdk.ButtonEvent) + } + + onLegacy?.(widget, event) + }) + } + + if (onMotion || onHoverEnter || onHoverLeave) { + const hover = new Gtk.EventControllerMotion + widget.add_controller(hover) + + if (onHoverEnter) + hover.connect("enter", (_, x, y) => onHoverEnter(widget, x, y)) + + if (onHoverLeave) + hover.connect("leave", () => onHoverLeave(widget)) + + if (onMotion) + hover.connect("motion", (_, x, y) => onMotion(widget, x, y)) + } + + if (onScroll || onScrollDecelerate) { + const scroll = new Gtk.EventControllerScroll + scroll.flags = Gtk.EventControllerScrollFlags.BOTH_AXES | Gtk.EventControllerScrollFlags.KINETIC + widget.add_controller(scroll) + + if (onScroll) + scroll.connect("scroll", (_, x, y) => onScroll(widget, x, y)) + + if (onScrollDecelerate) + scroll.connect("decelerate", (_, x, y) => onScrollDecelerate(widget, x, y)) + } + + return props +} diff --git a/lang/gjs/src/gtk4/index.ts b/lang/gjs/src/gtk4/index.ts index 3b1f737..51c75d2 100644 --- a/lang/gjs/src/gtk4/index.ts +++ b/lang/gjs/src/gtk4/index.ts @@ -1,9 +1,10 @@ import Astal from "gi://Astal?version=4.0" import Gtk from "gi://Gtk?version=4.0" import Gdk from "gi://Gdk?version=4.0" -// import astalify, { type ConstructProps } from "./astalify.js" +import astalify, { type ConstructProps } from "./astalify.js" export { Astal, Gtk, Gdk } export { default as App } from "./app.js" -// export { astalify, ConstructProps } -// export * as Widget from "./widget.js" +export { astalify, ConstructProps } +export * as Widget from "./widget.js" +export { hook } from "../_astal" diff --git a/lang/gjs/src/gtk4/jsx-runtime.ts b/lang/gjs/src/gtk4/jsx-runtime.ts index 6c8ea4d..80a3e87 100644 --- a/lang/gjs/src/gtk4/jsx-runtime.ts +++ b/lang/gjs/src/gtk4/jsx-runtime.ts @@ -1 +1,68 @@ -// TODO: +import Gtk from "gi://Gtk?version=4.0" +import { type BindableChild } from "./astalify.js" +import { mergeBindings, jsx as _jsx } from "../_astal.js" +import * as Widget from "./widget.js" + +export function Fragment({ children = [], child }: { + child?: BindableChild + children?: Array<BindableChild> +}) { + if (child) children.push(child) + return mergeBindings(children) +} + +export function jsx( + ctor: keyof typeof ctors | typeof Gtk.Widget, + props: any, +) { + return _jsx(ctors, ctor as any, props) +} + +const ctors = { + box: Widget.Box, + button: Widget.Button, + centerbox: Widget.CenterBox, + // circularprogress: Widget.CircularProgress, + // drawingarea: Widget.DrawingArea, + entry: Widget.Entry, + image: Widget.Image, + label: Widget.Label, + levelbar: Widget.LevelBar, + overlay: Widget.Overlay, + revealer: Widget.Revealer, + slider: Widget.Slider, + stack: Widget.Stack, + switch: Widget.Switch, + window: Widget.Window, + menubutton: Widget.MenuButton, + popover: Widget.Popover, +} + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace JSX { + type Element = Gtk.Widget + type ElementClass = Gtk.Widget + interface IntrinsicElements { + box: Widget.BoxProps + button: Widget.ButtonProps + centerbox: Widget.CenterBoxProps + // circularprogress: Widget.CircularProgressProps + // drawingarea: Widget.DrawingAreaProps + entry: Widget.EntryProps + image: Widget.ImageProps + label: Widget.LabelProps + levelbar: Widget.LevelBarProps + overlay: Widget.OverlayProps + revealer: Widget.RevealerProps + slider: Widget.SliderProps + stack: Widget.StackProps + switch: Widget.SwitchProps + window: Widget.WindowProps + menubutton: Widget.MenuButtonProps + popover: Widget.PopoverProps + } + } +} + +export const jsxs = jsx diff --git a/lang/gjs/src/gtk4/widget.ts b/lang/gjs/src/gtk4/widget.ts index 6c8ea4d..bd9091b 100644 --- a/lang/gjs/src/gtk4/widget.ts +++ b/lang/gjs/src/gtk4/widget.ts @@ -1 +1,166 @@ -// TODO: +import Astal from "gi://Astal?version=4.0" +import Gtk from "gi://Gtk?version=4.0" +import astalify, { type, type ConstructProps } from "./astalify.js" + +function filter(children: any[]) { + return children.flat(Infinity).map(ch => ch instanceof Gtk.Widget + ? ch + : new Gtk.Label({ visible: true, label: String(ch) })) +} + +// Box +Object.defineProperty(Astal.Box.prototype, "children", { + get() { return this.get_children() }, + set(v) { this.set_children(v) }, +}) + +export type BoxProps = ConstructProps<Astal.Box, Astal.Box.ConstructorProps> +export const Box = astalify<Astal.Box, Astal.Box.ConstructorProps>(Astal.Box, { + getChildren(self) { return self.get_children() }, + setChildren(self, children) { return self.set_children(filter(children)) }, +}) + +// Button +type ButtonSignals = { + onClicked: [] +} + +export type ButtonProps = ConstructProps<Gtk.Button, Gtk.Button.ConstructorProps, ButtonSignals> +export const Button = astalify<Gtk.Button, Gtk.Button.ConstructorProps, ButtonSignals>(Gtk.Button) + +// CenterBox +export type CenterBoxProps = ConstructProps<Gtk.CenterBox, Gtk.CenterBox.ConstructorProps> +export const CenterBox = astalify<Gtk.CenterBox, Gtk.CenterBox.ConstructorProps>(Gtk.CenterBox, { + getChildren(box) { + return [box.startWidget, box.centerWidget, box.endWidget] + }, + setChildren(box, children) { + const ch = filter(children) + box.startWidget = ch[0] || new Gtk.Box + box.centerWidget = ch[1] || new Gtk.Box + box.endWidget = ch[2] || new Gtk.Box + }, +}) + +// TODO: CircularProgress +// TODO: DrawingArea + +// Entry +type EntrySignals = { + onActivate: [] + onNotifyText: [] +} + +export type EntryProps = ConstructProps<Gtk.Entry, Gtk.Entry.ConstructorProps, EntrySignals> +export const Entry = astalify<Gtk.Entry, Gtk.Entry.ConstructorProps, EntrySignals>(Gtk.Entry, { + getChildren() { return [] }, +}) + +// Image +export type ImageProps = ConstructProps<Gtk.Image, Gtk.Image.ConstructorProps> +export const Image = astalify<Gtk.Image, Gtk.Image.ConstructorProps>(Gtk.Image, { + getChildren() { return [] }, +}) + +// Label +export type LabelProps = ConstructProps<Gtk.Label, Gtk.Label.ConstructorProps> +export const Label = astalify<Gtk.Label, Gtk.Label.ConstructorProps>(Gtk.Label, { + getChildren() { return [] }, + setChildren(self, children) { self.label = String(children) }, +}) + +// LevelBar +export type LevelBarProps = ConstructProps<Gtk.LevelBar, Gtk.LevelBar.ConstructorProps> +export const LevelBar = astalify<Gtk.LevelBar, Gtk.LevelBar.ConstructorProps>(Gtk.LevelBar, { + getChildren() { return [] }, +}) + +// TODO: ListBox + +// Overlay +export type OverlayProps = ConstructProps<Gtk.Overlay, Gtk.Overlay.ConstructorProps> +export const Overlay = astalify<Gtk.Overlay, Gtk.Overlay.ConstructorProps>(Gtk.Overlay, { + getChildren(self) { + const children: Array<Gtk.Widget> = [] + let ch = self.get_first_child() + while (ch !== null) { + children.push(ch) + ch = ch.get_next_sibling() + } + + return children.filter(ch => ch !== self.child) + }, + setChildren(self, children) { + for (const child of filter(children)) { + const types = type in child + ? (child[type] as string).split(/\s+/) + : [] + + if (types.includes("overlay")) { + self.add_overlay(child) + } else { + self.set_child(child) + } + + self.set_measure_overlay(child, types.includes("measure")) + self.set_clip_overlay(child, types.includes("clip")) + } + }, +}) + +// Revealer +export type RevealerProps = ConstructProps<Gtk.Revealer, Gtk.Revealer.ConstructorProps> +export const Revealer = astalify<Gtk.Revealer, Gtk.Revealer.ConstructorProps>(Gtk.Revealer) + +// Slider +type SliderSignals = { + onChangeValue: [] +} + +export type SliderProps = ConstructProps<Astal.Slider, Astal.Slider.ConstructorProps, SliderSignals> +export const Slider = astalify<Astal.Slider, Astal.Slider.ConstructorProps, SliderSignals>(Astal.Slider, { + getChildren() { return [] }, +}) + +// Stack +export type StackProps = ConstructProps<Gtk.Stack, Gtk.Stack.ConstructorProps> +export const Stack = astalify<Gtk.Stack, Gtk.Stack.ConstructorProps>(Gtk.Stack, { + setChildren(self, children) { + for (const child of filter(children)) { + if (child.name != "" && child.name != null) { + self.add_named(child, child.name) + } else { + self.add_child(child) + } + } + }, +}) + +// Switch +export type SwitchProps = ConstructProps<Gtk.Switch, Gtk.Switch.ConstructorProps> +export const Switch = astalify<Gtk.Switch, Gtk.Switch.ConstructorProps>(Gtk.Switch, { + getChildren() { return [] }, +}) + +// Window +export type WindowProps = ConstructProps<Astal.Window, Astal.Window.ConstructorProps> +export const Window = astalify<Astal.Window, Astal.Window.ConstructorProps>(Astal.Window) + +// MenuButton +export type MenuButtonProps = ConstructProps<Gtk.MenuButton, Gtk.MenuButton.ConstructorProps> +export const MenuButton = astalify<Gtk.MenuButton, Gtk.MenuButton.ConstructorProps>(Gtk.MenuButton, { + getChildren(self) { return [self.popover, self.child] }, + setChildren(self, children) { + for (const child of filter(children)) { + if (child instanceof Gtk.Popover) { + self.set_popover(child) + } else { + self.set_child(child) + } + } + }, +}) + +// Popoper +export type PopoverProps = ConstructProps<Gtk.Popover, Gtk.Popover.ConstructorProps> +export const Popover = astalify<Gtk.Popover, Gtk.Popover.ConstructorProps>(Gtk.Popover) diff --git a/lang/gjs/src/index.ts b/lang/gjs/src/index.ts index 8fe8d01..f448af9 100644 --- a/lang/gjs/src/index.ts +++ b/lang/gjs/src/index.ts @@ -4,5 +4,5 @@ export * from "./process.js" export * from "./time.js" export * from "./file.js" export * from "./gobject.js" -export { bind, default as Binding } from "./binding.js" -export { Variable } from "./variable.js" +export { Binding, bind } from "./binding.js" +export { Variable, derive } from "./variable.js" diff --git a/lang/gjs/src/overrides.ts b/lang/gjs/src/overrides.ts index 5b1ebad..e7eeba2 100644 --- a/lang/gjs/src/overrides.ts +++ b/lang/gjs/src/overrides.ts @@ -37,6 +37,7 @@ await suppress(import("gi://AstalBluetooth"), ({ Adapter, Bluetooth, Device }) = }) await suppress(import("gi://AstalHyprland"), ({ Hyprland, Monitor, Workspace }) => { + patch(Hyprland.prototype, "binds") patch(Hyprland.prototype, "monitors") patch(Hyprland.prototype, "workspaces") patch(Hyprland.prototype, "clients") @@ -47,8 +48,8 @@ await suppress(import("gi://AstalHyprland"), ({ Hyprland, Monitor, Workspace }) await suppress(import("gi://AstalMpris"), ({ Mpris, Player }) => { patch(Mpris.prototype, "players") - patch(Player.prototype, "supported_uri_schemas") - patch(Player.prototype, "supportedUriSchemas") + patch(Player.prototype, "supported_uri_schemes") + patch(Player.prototype, "supportedUriSchemes") patch(Player.prototype, "supported_mime_types") patch(Player.prototype, "supportedMimeTypes") patch(Player.prototype, "comments") diff --git a/lang/gjs/src/package.json b/lang/gjs/src/package.json new file mode 100644 index 0000000..b792213 --- /dev/null +++ b/lang/gjs/src/package.json @@ -0,0 +1,22 @@ +{ + "name": "astal", + "type": "module", + "license": "LGPL-2.1", + "exports": { + ".": "./index.ts", + "./gtk3": "./gtk3/index.ts", + "./gtk4": "./gtk4/index.ts", + "./gtk3/app": "./gtk3/app.ts", + "./gtk4/app": "./gtk4/app.ts", + "./gtk3/widget": "./gtk3/widget.ts", + "./gtk4/widget": "./gtk4/widget.ts", + "./gtk3/jsx-runtime": "./gtk3/jsx-runtime.ts", + "./gtk4/jsx-runtime": "./gtk4/jsx-runtime.ts", + "./binding": "./binding.ts", + "./file": "./file.ts", + "./gobject": "./gobject.ts", + "./process": "./process.ts", + "./time": "./time.ts", + "./variable": "./variable.ts" + } +} diff --git a/lang/gjs/src/process.ts b/lang/gjs/src/process.ts index c41adc1..dd4a6ae 100644 --- a/lang/gjs/src/process.ts +++ b/lang/gjs/src/process.ts @@ -6,7 +6,8 @@ type Args = { err?: (stderr: string) => void } -export const { Process } = Astal +export type Process = Astal.Process +export const Process = Astal.Process export function subprocess(args: Args): Astal.Process @@ -50,18 +51,15 @@ export function execAsync(cmd: string | string[]): Promise<string> { Astal.Process.exec_asyncv(cmd, (_, res) => { try { resolve(Astal.Process.exec_asyncv_finish(res)) - } - catch (error) { + } catch (error) { reject(error) } }) - } - else { + } else { Astal.Process.exec_async(cmd, (_, res) => { try { resolve(Astal.Process.exec_finish(res)) - } - catch (error) { + } catch (error) { reject(error) } }) diff --git a/lang/gjs/src/time.ts b/lang/gjs/src/time.ts index 1939d98..b581aa9 100644 --- a/lang/gjs/src/time.ts +++ b/lang/gjs/src/time.ts @@ -1,6 +1,7 @@ import Astal from "gi://AstalIO" -export const { Time } = Astal +export type Time = Astal.Time +export const Time = Astal.Time export function interval(interval: number, callback?: () => void) { return Astal.Time.interval(interval, () => void callback?.()) diff --git a/lang/gjs/src/variable.ts b/lang/gjs/src/variable.ts index 9b3d3d2..016d73a 100644 --- a/lang/gjs/src/variable.ts +++ b/lang/gjs/src/variable.ts @@ -60,13 +60,11 @@ class VariableWrapper<T> extends Function { if (v instanceof Promise) { v.then(v => this.set(v)) .catch(err => this.variable.emit("error", err)) - } - else { + } else { this.set(v) } }) - } - else if (this.pollExec) { + } else if (this.pollExec) { this._poll = interval(this.pollInterval, () => { execAsync(this.pollExec!) .then(v => this.set(this.pollTransform!(v, this.get()))) @@ -143,8 +141,7 @@ class VariableWrapper<T> extends Function { if (typeof exec === "function") { this.pollFn = exec delete this.pollExec - } - else { + } else { this.pollExec = exec delete this.pollFn } @@ -188,8 +185,7 @@ class VariableWrapper<T> extends Function { const id = o.connect(s, set) this.onDropped(() => o.disconnect(id)) } - } - else { + } else { if (typeof sigOrFn === "string") { const id = objs.connect(sigOrFn, set) this.onDropped(() => objs.disconnect(id)) @@ -227,4 +223,5 @@ export const Variable = new Proxy(VariableWrapper as any, { new<T>(init: T): Variable<T> } +export const { derive } = Variable export default Variable diff --git a/lang/lua/astal/gtk3/astalify.lua b/lang/lua/astal/gtk3/astalify.lua index 95faa2c..f1a280b 100644 --- a/lang/lua/astal/gtk3/astalify.lua +++ b/lang/lua/astal/gtk3/astalify.lua @@ -52,15 +52,20 @@ local function includes(tbl, elem) end local function set_children(parent, children) - children = map(flatten(children), function(item) - if Gtk.Widget:is_type_of(item) then - return item - end - return Gtk.Label({ - visible = true, - label = tostring(item), - }) - end) + children = map( + filter(flatten(children), function(item) + return not not item + end), + function(item) + if Gtk.Widget:is_type_of(item) then + return item + end + return Gtk.Label({ + visible = true, + label = tostring(item), + }) + end + ) -- remove if Gtk.Bin:is_type_of(parent) then @@ -174,7 +179,7 @@ return function(ctor) end))) -- default visible to true - if type(tbl.visible) ~= "boolean" then + if tbl.visible == nil then tbl.visible = true end diff --git a/lang/lua/astal/gtk3/widget.lua b/lang/lua/astal/gtk3/widget.lua index c8857e7..654da29 100644 --- a/lang/lua/astal/gtk3/widget.lua +++ b/lang/lua/astal/gtk3/widget.lua @@ -19,6 +19,7 @@ local Widget = { Label = astalify(Gtk.Label), LevelBar = astalify(Astal.LevelBar), -- TODO: ListBox + MenuButton = astalify(Gtk.MenuButton), Overlay = astalify(Astal.Overlay), Revealer = astalify(Gtk.Revealer), Scrollable = astalify(Astal.Scrollable), @@ -64,6 +65,12 @@ Gtk.Widget._attribute.click_through = { set = Astal.widget_set_click_through, } +Gtk.Widget._attribute.action_group = { + set = function (self, v) + self:insert_action_group(v[1], v[2]) + end +} + local no_implicit_destroy = {} Gtk.Widget._attribute.no_implicit_destroy = { get = function(self) diff --git a/lang/lua/astal/variable.lua b/lang/lua/astal/variable.lua index ad59a3f..5832d8a 100644 --- a/lang/lua/astal/variable.lua +++ b/lang/lua/astal/variable.lua @@ -260,7 +260,9 @@ function Variable.derive(deps, transform) local unsubs = {} for i, b in ipairs(deps) do - unsubs[i] = b:subscribe(update) + unsubs[i] = b:subscribe(function() + var:set(update()) + end) end var.variable.on_dropped = function() diff --git a/lib/apps/default.nix b/lib/apps/default.nix new file mode 100644 index 0000000..64466a2 --- /dev/null +++ b/lib/apps/default.nix @@ -0,0 +1,15 @@ +{ + mkAstalPkg, + pkgs, + ... +}: +mkAstalPkg { + pname = "astal-apps"; + src = ./.; + packages = [pkgs.json-glib]; + + libname = "apps"; + gir-suffix = "Apps"; + authors = "Aylur"; + description = "Application query library"; +} diff --git a/lib/apps/fuzzy.vala b/lib/apps/fuzzy.vala index 7745320..590112e 100644 --- a/lib/apps/fuzzy.vala +++ b/lib/apps/fuzzy.vala @@ -2,15 +2,15 @@ namespace AstalApps { private int fuzzy_match_string(string pattern, string str) { const int unmatched_letter_penalty = -1; int score = 100; + int not_found_score = -10; if (pattern.length == 0) return score; - if (str.length < pattern.length) return int.MIN; + if (str.length < pattern.length) return not_found_score; bool found = fuzzy_match_recurse(pattern, str, score, true, out score); score += unmatched_letter_penalty * (str.length - pattern.length); - if(!found) score = -10; - + if(!found) score = not_found_score; return score; } diff --git a/lib/astal/gtk3/default.nix b/lib/astal/gtk3/default.nix new file mode 100644 index 0000000..c86f3de --- /dev/null +++ b/lib/astal/gtk3/default.nix @@ -0,0 +1,21 @@ +{ + mkAstalPkg, + pkgs, + self, +}: +mkAstalPkg { + pname = "astal3"; + src = ./.; + packages = [ + self.packages.${pkgs.system}.io + pkgs.gtk3 + pkgs.gtk-layer-shell + ]; + + libname = "astal3"; + gir-suffix = ""; + authors = "Aylur"; + description = "Astal GTK3 widget library"; + dependencies = ["AstalIO-0.1" "Gtk-3.0"]; + repo-path = "astal/gtk3"; +} diff --git a/lib/astal/gtk3/src/config.vala.in b/lib/astal/gtk3/src/config.vala.in index 88bfe9c..a141571 100644 --- a/lib/astal/gtk3/src/config.vala.in +++ b/lib/astal/gtk3/src/config.vala.in @@ -1,3 +1,4 @@ +[CCode (gir_namespace = "Astal", gir_version = "@API_VERSION@")] namespace Astal { public const int MAJOR_VERSION = @MAJOR_VERSION@; public const int MINOR_VERSION = @MINOR_VERSION@; diff --git a/lib/astal/gtk3/src/meson.build b/lib/astal/gtk3/src/meson.build index bf8f72a..2587b10 100644 --- a/lib/astal/gtk3/src/meson.build +++ b/lib/astal/gtk3/src/meson.build @@ -10,6 +10,7 @@ config = configure_file( input: 'config.vala.in', output: 'config.vala', configuration: { + 'API_VERSION': api_version, 'VERSION': meson.project_version(), 'MAJOR_VERSION': version_split[0], 'MINOR_VERSION': version_split[1], diff --git a/lib/astal/gtk3/src/widget/circularprogress.vala b/lib/astal/gtk3/src/widget/circularprogress.vala index df1635d..de7a5c7 100644 --- a/lib/astal/gtk3/src/widget/circularprogress.vala +++ b/lib/astal/gtk3/src/widget/circularprogress.vala @@ -41,7 +41,7 @@ public class Astal.CircularProgress : Gtk.Bin { } static construct { - set_css_name("circular-progress"); + set_css_name("circularprogress"); } public override Gtk.SizeRequestMode get_request_mode() { diff --git a/lib/astal/gtk3/src/widget/icon.vala b/lib/astal/gtk3/src/widget/icon.vala index 9a20359..ee6808c 100644 --- a/lib/astal/gtk3/src/widget/icon.vala +++ b/lib/astal/gtk3/src/widget/icon.vala @@ -9,7 +9,21 @@ public class Astal.Icon : Gtk.Image { private double size { get; set; default = 14; } public new Gdk.Pixbuf pixbuf { get; set; } - public GLib.Icon g_icon { get; set; } + + private static bool gicon_warned = false; + [Version (deprecated = true, deprecated_since = "0.1.0", replacement = "gicon")] + public GLib.Icon g_icon { + owned get { + return this.gicon; + } + set { + if( !gicon_warned ) { + GLib.warning("g-icon is deprecated. Use gicon instead."); + gicon_warned = true; + } + this.gicon = value; + } + } /** * Either a named icon or a path to a file. @@ -57,7 +71,6 @@ public class Astal.Icon : Gtk.Image { break; case IconType.GICON: pixel_size = (int)size; - gicon = g_icon; break; } @@ -86,7 +99,7 @@ public class Astal.Icon : Gtk.Image { display_icon.begin(); }); - notify["g-icon"].connect(() => { + notify["gicon"].connect(() => { type = IconType.GICON; display_icon.begin(); }); diff --git a/lib/astal/gtk3/src/widget/slider.vala b/lib/astal/gtk3/src/widget/slider.vala index 97cfb69..b927e32 100644 --- a/lib/astal/gtk3/src/widget/slider.vala +++ b/lib/astal/gtk3/src/widget/slider.vala @@ -30,6 +30,10 @@ public class Astal.Slider : Gtk.Scale { step = 0.05; } + if (page == 0) { + page = 0.01; + } + notify["orientation"].connect(() => { notify_property("vertical"); }); @@ -90,5 +94,13 @@ public class Astal.Slider : Gtk.Scale { set { adjustment.step_increment = value; } } + /** + * Size of page increments. Defaults to `0.01`. + */ + public double page { + get { return adjustment.page_increment; } + set { adjustment.page_increment = value; } + } + // TODO: marks } diff --git a/lib/astal/gtk3/src/widget/window.vala b/lib/astal/gtk3/src/widget/window.vala index 11d542d..a066806 100644 --- a/lib/astal/gtk3/src/widget/window.vala +++ b/lib/astal/gtk3/src/widget/window.vala @@ -50,6 +50,13 @@ public class Astal.Window : Gtk.Window { private InhibitManager? inhibit_manager; private Inhibitor? inhibitor; + /** + * Get the current [[email protected]] this window resides in. + */ + public Gdk.Monitor get_current_monitor() { + return Gdk.Display.get_default().get_monitor_at_window(base.get_window()); + } + private bool check(string action) { if (!is_supported()) { critical(@"can not $action on window: layer shell not supported"); @@ -63,7 +70,7 @@ public class Astal.Window : Gtk.Window { } construct { - // If the window has no size allocatoted when it gets mapped. + // If the window has no size allocatoted when it gets mapped // It won't show up later either when it size changes by adding children. height_request = 1; width_request = 1; diff --git a/lib/astal/gtk4/default.nix b/lib/astal/gtk4/default.nix new file mode 100644 index 0000000..0bca3c0 --- /dev/null +++ b/lib/astal/gtk4/default.nix @@ -0,0 +1,21 @@ +{ + mkAstalPkg, + pkgs, + self, +}: +mkAstalPkg { + pname = "astal4"; + src = ./.; + packages = [ + self.packages.${pkgs.system}.io + pkgs.gtk4 + pkgs.gtk4-layer-shell + ]; + + libname = "astal4"; + gir-suffix = ""; + authors = "Aylur"; + description = "Astal GTK4 widget library"; + dependencies = ["AstalIO-0.1" "Gtk-4.0"]; + repo-path = "astal/gtk4"; +} diff --git a/lib/astal/gtk4/src/application.vala b/lib/astal/gtk4/src/application.vala index fadf705..99f18f8 100644 --- a/lib/astal/gtk4/src/application.vala +++ b/lib/astal/gtk4/src/application.vala @@ -143,16 +143,22 @@ public class Astal.Application : Gtk.Application, AstalIO.Application { public void apply_css(string style, bool reset = false) { var provider = new Gtk.CssProvider(); + provider.parsing_error.connect((section, error) => { + critical("CSS Error %s:%zu:%zu: %s\n", + section.get_file()?.get_basename() ?? "", + section.get_start_location().lines + 1, + section.get_start_location().line_chars + 1, + error.message + ); + }); + if (reset) reset_css(); - try { - if (FileUtils.test(style, FileTest.EXISTS)) - provider.load_from_path(style); - else - provider.load_from_string(style); - } catch (Error err) { - critical(err.message); + if (FileUtils.test(style, FileTest.EXISTS)) { + provider.load_from_path(style); + } else { + provider.load_from_string(style); } Gtk.StyleContext.add_provider_for_display( diff --git a/lib/astal/gtk4/src/config.vala.in b/lib/astal/gtk4/src/config.vala.in index 88bfe9c..a141571 100644 --- a/lib/astal/gtk4/src/config.vala.in +++ b/lib/astal/gtk4/src/config.vala.in @@ -1,3 +1,4 @@ +[CCode (gir_namespace = "Astal", gir_version = "@API_VERSION@")] namespace Astal { public const int MAJOR_VERSION = @MAJOR_VERSION@; public const int MINOR_VERSION = @MINOR_VERSION@; diff --git a/lib/astal/gtk4/src/meson.build b/lib/astal/gtk4/src/meson.build index 8aac969..58408a6 100644 --- a/lib/astal/gtk4/src/meson.build +++ b/lib/astal/gtk4/src/meson.build @@ -10,6 +10,7 @@ config = configure_file( input: 'config.vala.in', output: 'config.vala', configuration: { + 'API_VERSION': api_version, 'VERSION': meson.project_version(), 'MAJOR_VERSION': version_split[0], 'MINOR_VERSION': version_split[1], @@ -25,6 +26,8 @@ deps = [ ] sources = [config] + files( + 'widget/box.vala', + 'widget/slider.vala', 'widget/window.vala', 'application.vala', ) diff --git a/lib/astal/gtk4/src/widget/box.vala b/lib/astal/gtk4/src/widget/box.vala new file mode 100644 index 0000000..28f2b00 --- /dev/null +++ b/lib/astal/gtk4/src/widget/box.vala @@ -0,0 +1,50 @@ +public class Astal.Box : Gtk.Box { + /** + * Corresponds to [[email protected] :orientation]. + */ + [CCode (notify = false)] + public bool vertical { + get { return orientation == Gtk.Orientation.VERTICAL; } + set { orientation = value ? Gtk.Orientation.VERTICAL : Gtk.Orientation.HORIZONTAL; } + } + + construct { + notify["orientation"].connect(() => { + notify_property("vertical"); + }); + } + + public List<weak Gtk.Widget> children { + set { + foreach (var child in children) { + remove(child); + } + foreach (var child in value) { + append(child); + } + } + owned get { + var list = new List<weak Gtk.Widget>(); + var child = get_first_child(); + while (child != null) { + list.append(child); + child = child.get_next_sibling(); + } + return list; + } + } + + public Gtk.Widget? child { + owned get { + foreach (var child in children) { + return child; + } + return null; + } + set { + var list = new List<weak Gtk.Widget>(); + list.append(child); + this.children = children; + } + } +} diff --git a/lib/astal/gtk4/src/widget/slider.vala b/lib/astal/gtk4/src/widget/slider.vala new file mode 100644 index 0000000..371f5be --- /dev/null +++ b/lib/astal/gtk4/src/widget/slider.vala @@ -0,0 +1,77 @@ +public class Astal.Slider : Gtk.Scale { + private Gtk.EventControllerLegacy controller; + private bool dragging; + + construct { + if (adjustment == null) + adjustment = new Gtk.Adjustment(0,0,0,0,0,0); + + if (max == 0 && min == 0) { + max = 1; + } + + if (step == 0) { + step = 0.05; + } + + if (page == 0) { + page = 0.01; + } + + controller = new Gtk.EventControllerLegacy(); + add_controller(controller); + controller.event.connect((event) => { + var type = event.get_event_type(); + if (type == Gdk.EventType.BUTTON_PRESS || + type == Gdk.EventType.KEY_PRESS || + type == Gdk.EventType.TOUCH_BEGIN) { + dragging = true; + } + if (type == Gdk.EventType.BUTTON_RELEASE || + type == Gdk.EventType.KEY_RELEASE || + type == Gdk.EventType.TOUCH_END) { + dragging = false; + } + }); + } + + /** + * Value of this slider. Defaults to `0`. + */ + public double value { + get { return adjustment.value; } + set { if (!dragging) adjustment.value = value; } + } + + /** + * Minimum possible value of this slider. Defaults to `0`. + */ + public double min { + get { return adjustment.lower; } + set { adjustment.lower = value; } + } + + /** + * Maximum possible value of this slider. Defaults to `1`. + */ + public double max { + get { return adjustment.upper; } + set { adjustment.upper = value; } + } + + /** + * Size of step increments. Defaults to `0.05`. + */ + public double step { + get { return adjustment.step_increment; } + set { adjustment.step_increment = value; } + } + + /** + * Size of page increments. Defaults to `0.01`. + */ + public double page { + get { return adjustment.page_increment; } + set { adjustment.page_increment = value; } + } +} diff --git a/lib/astal/gtk4/src/widget/window.vala b/lib/astal/gtk4/src/widget/window.vala index 0cf3d11..3b0d113 100644 --- a/lib/astal/gtk4/src/widget/window.vala +++ b/lib/astal/gtk4/src/widget/window.vala @@ -47,6 +47,13 @@ public enum Astal.Keymode { * Subclass of [[email protected]] which integrates GtkLayerShell as class fields. */ public class Astal.Window : Gtk.Window { + /** + * Get the current [[email protected]] this window resides in. + */ + public Gdk.Monitor get_current_monitor() { + return Gdk.Display.get_default().get_monitor_at_surface(base.get_surface()); + } + private bool check(string action) { if (!is_supported()) { critical(@"can not $action on window: layer shell not supported"); diff --git a/lib/astal/io/config.vala.in b/lib/astal/io/config.vala.in index fe1e450..4aee790 100644 --- a/lib/astal/io/config.vala.in +++ b/lib/astal/io/config.vala.in @@ -1,3 +1,4 @@ +[CCode (gir_namespace = "AstalIO", gir_version = "@API_VERSION@")] namespace AstalIO { public const int MAJOR_VERSION = @MAJOR_VERSION@; public const int MINOR_VERSION = @MINOR_VERSION@; diff --git a/lib/astal/io/daemon.vala b/lib/astal/io/daemon.vala new file mode 100644 index 0000000..7f3173e --- /dev/null +++ b/lib/astal/io/daemon.vala @@ -0,0 +1,88 @@ +[DBus (name="io.Astal.Application")] +public class AstalIO.Daemon : GLib.Application, AstalIO.Application { + private SocketService service; + private DBusConnection conn; + private string _instance_name = "astal"; + private string socket_path { get; private set; } + + /** + * A unique instance name. + * + * This is the identifier used by the AstalIO package and the CLI. + */ + [DBus (visible=false)] + public string instance_name { + owned get { return _instance_name; } + construct set { + _instance_name = value != null ? value : "astal"; + application_id = @"io.Astal.$_instance_name"; + } + } + + /** + * Handler for an incoming request. + * + * @param msg Body of the message + * @param conn The connection which expects the response. + */ + [DBus (visible=false)] + public virtual void request(string msg, SocketConnection conn) { + AstalIO.write_sock.begin(conn, @"missing response implementation on $application_id"); + } + + /** + * Attempt to acquire the astal socket for this app identified by its [[email protected]:instance_name]. + * If the socket is in use by another app with the same name an [[email protected]_OCCUPIED] is thrown. + */ + [DBus (visible=false)] + public void acquire_socket() throws Error { + string path; + service = AstalIO.acquire_socket(this, out path); + socket_path = path; + + Bus.own_name( + BusType.SESSION, + application_id, + BusNameOwnerFlags.NONE, + (conn) => { + try { + this.conn = conn; + conn.register_object("/io/Astal/Application", this); + } catch (Error err) { + critical(err.message); + } + }, + () => {}, + () => {} + ); + } + + public void inspector() throws DBusError, IOError { + throw new DBusError.FAILED("Daemon does not implement inspector"); + } + + public void toggle_window(string window) throws DBusError, IOError { + throw new DBusError.FAILED("Daemon does not implement toggle_window"); + } + + /** + * Quit and stop the socket if it was acquired. + */ + public new void quit() throws DBusError, IOError { + if (service != null) { + service.stop(); + service.close(); + } + + base.quit(); + } + + construct { + hold(); + + shutdown.connect(() => { try { quit(); } catch(Error err) {} }); + Unix.signal_add(1, () => { try { quit(); } catch(Error err) {} }, Priority.HIGH); + Unix.signal_add(2, () => { try { quit(); } catch(Error err) {} }, Priority.HIGH); + Unix.signal_add(15, () => { try { quit(); } catch(Error err) {} }, Priority.HIGH); + } +} diff --git a/lib/astal/io/default.nix b/lib/astal/io/default.nix new file mode 100644 index 0000000..c33132a --- /dev/null +++ b/lib/astal/io/default.nix @@ -0,0 +1,12 @@ +{mkAstalPkg, ...}: +mkAstalPkg { + pname = "astal"; + src = ./.; + + libname = "io"; + gir-suffix = "IO"; + authors = "Aylur"; + description = "Astal Core library"; + repo-path = "astal/io"; + website-path = "io"; +} diff --git a/lib/astal/io/file.vala b/lib/astal/io/file.vala index 57b6dc0..e8e8a8b 100644 --- a/lib/astal/io/file.vala +++ b/lib/astal/io/file.vala @@ -26,6 +26,11 @@ public async string read_file_async(string path) throws Error { */ public void write_file(string path, string content) { try { + var dir = Path.get_dirname(path); + if (!FileUtils.test(dir, FileTest.IS_DIR)) { + File.new_for_path(dir).make_directory_with_parents(null); + } + FileUtils.set_contents(path, content); } catch (Error error) { critical(error.message); @@ -36,6 +41,11 @@ public void write_file(string path, string content) { * Write content to a file asynchronously. */ public async void write_file_async(string path, string content) throws Error { + var dir = Path.get_dirname(path); + if (!FileUtils.test(dir, FileTest.IS_DIR)) { + File.new_for_path(dir).make_directory_with_parents(null); + } + yield File.new_for_path(path).replace_contents_async( content.data, null, diff --git a/lib/astal/io/meson.build b/lib/astal/io/meson.build index 023dece..9a00904 100644 --- a/lib/astal/io/meson.build +++ b/lib/astal/io/meson.build @@ -22,6 +22,7 @@ config = configure_file( input: 'config.vala.in', output: 'config.vala', configuration: { + 'API_VERSION': api_version, 'VERSION': meson.project_version(), 'MAJOR_VERSION': version_split[0], 'MINOR_VERSION': version_split[1], @@ -38,6 +39,7 @@ deps = [ sources = [config] + files( 'application.vala', + 'daemon.vala', 'file.vala', 'process.vala', 'time.vala', diff --git a/lib/astal/io/process.vala b/lib/astal/io/process.vala index cfd05b9..95c67a3 100644 --- a/lib/astal/io/process.vala +++ b/lib/astal/io/process.vala @@ -2,20 +2,22 @@ * `Process` provides shortcuts for [[email protected]] with sane defaults. */ public class AstalIO.Process : Object { + public string[] argv { construct; get; } + private void read_stream(DataInputStream stream, bool err) { stream.read_line_utf8_async.begin(Priority.DEFAULT, null, (_, res) => { try { var output = stream.read_line_utf8_async.end(res); if (output != null) { - if (err) + if (err) { stdout(output.strip()); - else + } else { stderr(output.strip()); - + } read_stream(stream, err); } } catch (Error err) { - printerr("%s\n", err.message); + critical(err.message); } }); } @@ -24,20 +26,27 @@ public class AstalIO.Process : Object { private DataInputStream err_stream; private DataOutputStream in_stream; private Subprocess process; - public string[] argv { construct; get; } + /** + * When the underlying subprocess writes to its stdout. + * + * @param out Line written to stdout + */ + public signal void stdout(string out); /** - * When the underlying subprocess writes to its stdout - * this signal is emitted with that line. + * When the underlying subprocess writes to its stderr. + * + * @param err Line written to stderr */ - public signal void stdout (string out); + public signal void stderr(string err); /** - * When the underlying subprocess writes to its stderr - * this signal is emitted with that line. + * When the underlying subprocess exits or is terminated. + * + * @param code Exit code or signal number if terminated */ - public signal void stderr (string err); + public signal void exit(int code, bool terminated); /** * Force quit the subprocess. @@ -48,6 +57,8 @@ public class AstalIO.Process : Object { /** * Send a signal to the subprocess. + * + * @param signal_num Signal number to be sent */ public void signal(int signal_num) { process.send_signal(signal_num); @@ -55,6 +66,8 @@ public class AstalIO.Process : Object { /** * Write a line to the subprocess' stdin synchronously. + * + * @param in String to be written to stdin */ public void write(string in) throws Error { in_stream.put_string(in); @@ -62,6 +75,8 @@ public class AstalIO.Process : Object { /** * Write a line to the subprocess' stdin asynchronously. + * + * @param in String to be written to stdin */ public async void write_async(string in) { try { @@ -71,23 +86,47 @@ public class AstalIO.Process : Object { } } - /** - * Start a new subprocess with the given command. - * - * The first element of the vector is executed with the remaining elements as the argument list. - */ - public Process.subprocessv(string[] cmd) throws Error { + /** See [[email protected]] */ + public Process(string[] cmd) throws Error { Object(argv: cmd); - process = new Subprocess.newv(cmd, + process = new Subprocess.newv( + cmd, SubprocessFlags.STDIN_PIPE | SubprocessFlags.STDERR_PIPE | SubprocessFlags.STDOUT_PIPE ); + out_stream = new DataInputStream(process.get_stdout_pipe()); err_stream = new DataInputStream(process.get_stderr_pipe()); in_stream = new DataOutputStream(process.get_stdin_pipe()); + read_stream(out_stream, true); read_stream(err_stream, false); + + process.wait_async.begin(null, (_, res) => { + try { + process.wait_async.end(res); + } catch (Error err) { + // ignore + } + + if (process.get_if_exited()) { + exit(process.get_exit_status(), false); + } + + if (process.get_if_signaled()) { + exit(process.get_term_sig(), true); + } + }); + } + + /** + * Start a new subprocess with the given command. + * + * The first element of the vector is executed with the remaining elements as the argument list. + */ + public static Process subprocessv(string[] cmd) throws Error { + return new Process(cmd); } /** @@ -97,7 +136,7 @@ public class AstalIO.Process : Object { public static Process subprocess(string cmd) throws Error { string[] argv; Shell.parse_argv(cmd, out argv); - return new Process.subprocessv(argv); + return Process.subprocessv(argv); } /** @@ -116,11 +155,11 @@ public class AstalIO.Process : Object { string err_str, out_str; process.communicate_utf8(null, null, out out_str, out err_str); var success = process.get_successful(); - process.dispose(); - if (success) + if (success) { return out_str.strip(); - else + } else { throw new IOError.FAILED(err_str.strip()); + } } /** @@ -151,11 +190,11 @@ public class AstalIO.Process : Object { string err_str, out_str; yield process.communicate_utf8_async(null, null, out out_str, out err_str); var success = process.get_successful(); - process.dispose(); - if (success) + if (success) { return out_str.strip(); - else + } else { throw new IOError.FAILED(err_str.strip()); + } } /** diff --git a/lib/astal/io/variable.vala b/lib/astal/io/variable.vala index 312a27a..e4105f8 100644 --- a/lib/astal/io/variable.vala +++ b/lib/astal/io/variable.vala @@ -172,7 +172,7 @@ public class AstalIO.Variable : VariableBase { return_if_fail(watch_proc == null); return_if_fail(watch_exec != null); - watch_proc = new Process.subprocessv(watch_exec); + watch_proc = Process.subprocessv(watch_exec); watch_proc.stdout.connect((str) => set_closure(str, watch_transform)); watch_proc.stderr.connect((str) => this.error(str)); } diff --git a/lib/auth/default.nix b/lib/auth/default.nix new file mode 100644 index 0000000..3b8b276 --- /dev/null +++ b/lib/auth/default.nix @@ -0,0 +1,15 @@ +{ + mkAstalPkg, + pkgs, + ... +}: +mkAstalPkg { + pname = "astal-auth"; + src = ./.; + packages = [pkgs.pam]; + + libname = "auth"; + gir-suffix = "Auth"; + authors = "kotontrion"; + description = "Authentication using pam"; +} diff --git a/lib/battery/config.vala.in b/lib/battery/config.vala.in index 6e7f77e..404e60a 100644 --- a/lib/battery/config.vala.in +++ b/lib/battery/config.vala.in @@ -1,3 +1,4 @@ +[CCode (gir_namespace = "AstalBattery", gir_version = "@API_VERSION@")] namespace AstalBattery { public const int MAJOR_VERSION = @MAJOR_VERSION@; public const int MINOR_VERSION = @MINOR_VERSION@; diff --git a/lib/battery/default.nix b/lib/battery/default.nix new file mode 100644 index 0000000..17bf67a --- /dev/null +++ b/lib/battery/default.nix @@ -0,0 +1,15 @@ +{ + mkAstalPkg, + pkgs, + ... +}: +mkAstalPkg { + pname = "astal-battery"; + src = ./.; + packages = [pkgs.json-glib]; + + libname = "battery"; + authors = "Aylur"; + gir-suffix = "Battery"; + description = "DBus proxy for upowerd devices"; +} diff --git a/lib/battery/meson.build b/lib/battery/meson.build index 054e9db..ec8e3d0 100644 --- a/lib/battery/meson.build +++ b/lib/battery/meson.build @@ -25,6 +25,7 @@ config = configure_file( input: 'config.vala.in', output: 'config.vala', configuration: { + 'API_VERSION': api_version, 'VERSION': meson.project_version(), 'MAJOR_VERSION': version_split[0], 'MINOR_VERSION': version_split[1], diff --git a/lib/bluetooth/config.vala.in b/lib/bluetooth/config.vala.in index 9fce720..f43dba3 100644 --- a/lib/bluetooth/config.vala.in +++ b/lib/bluetooth/config.vala.in @@ -1,3 +1,4 @@ +[CCode (gir_namespace = "AstalBluetooth", gir_version = "@API_VERSION@")] namespace AstalBluetooth { public const int MAJOR_VERSION = @MAJOR_VERSION@; public const int MINOR_VERSION = @MINOR_VERSION@; diff --git a/lib/bluetooth/default.nix b/lib/bluetooth/default.nix new file mode 100644 index 0000000..f2d58c8 --- /dev/null +++ b/lib/bluetooth/default.nix @@ -0,0 +1,10 @@ +{mkAstalPkg, ...}: +mkAstalPkg { + pname = "astal-bluetooth"; + src = ./.; + + libname = "bluetooth"; + authors = "Aylur"; + gir-suffix = "Bluetooth"; + description = "DBus proxy for bluez"; +} diff --git a/lib/bluetooth/meson.build b/lib/bluetooth/meson.build index 5288a9e..529e85f 100644 --- a/lib/bluetooth/meson.build +++ b/lib/bluetooth/meson.build @@ -20,6 +20,7 @@ config = configure_file( input: 'config.vala.in', output: 'config.vala', configuration: { + 'API_VERSION': api_version, 'VERSION': meson.project_version(), 'MAJOR_VERSION': version_split[0], 'MINOR_VERSION': version_split[1], diff --git a/lib/cava/cava.c b/lib/cava/cava.c index 1c5ef66..2934335 100644 --- a/lib/cava/cava.c +++ b/lib/cava/cava.c @@ -440,7 +440,7 @@ static void astal_cava_cava_get_property(GObject* object, guint property_id, GVa g_value_set_int(value, self->bars); break; case ASTAL_CAVA_CAVA_PROP_VALUES: - g_value_set_pointer(value, self->values); + g_value_set_boxed(value, self->values); break; case ASTAL_CAVA_CAVA_PROP_AUTOSENS: g_value_set_boolean(value, self->autosens); @@ -558,7 +558,7 @@ static void astal_cava_cava_class_init(AstalCavaCavaClass* class) { * be 0. */ astal_cava_cava_properties[ASTAL_CAVA_CAVA_PROP_VALUES] = - g_param_spec_pointer("values", "values", "a list of values", G_PARAM_READABLE); + g_param_spec_boxed("values", "values", "a list of values", G_TYPE_ARRAY, G_PARAM_READABLE); /** * AstalCavaCava:active: * diff --git a/lib/cava/default.nix b/lib/cava/default.nix new file mode 100644 index 0000000..5472e8c --- /dev/null +++ b/lib/cava/default.nix @@ -0,0 +1,56 @@ +{ + mkAstalPkg, + pkgs, + ... +}: let + libcava = pkgs.stdenv.mkDerivation rec { + pname = "cava"; + version = "0.10.3"; + + src = pkgs.fetchFromGitHub { + owner = "LukashonakV"; + repo = "cava"; + rev = "0.10.3"; + hash = "sha256-ZDFbI69ECsUTjbhlw2kHRufZbQMu+FQSMmncCJ5pagg="; + }; + + buildInputs = with pkgs; [ + alsa-lib + libpulseaudio + ncurses + iniparser + sndio + SDL2 + libGL + portaudio + jack2 + pipewire + ]; + + propagatedBuildInputs = with pkgs; [ + fftw + ]; + + nativeBuildInputs = with pkgs; [ + autoreconfHook + autoconf-archive + pkgconf + meson + ninja + ]; + + preAutoreconf = '' + echo ${version} > version + ''; + }; +in + mkAstalPkg { + pname = "astal-cava"; + src = ./.; + packages = [libcava]; + + libname = "cava"; + authors = "kotontrion"; + gir-suffix = "Cava"; + description = "Audio visualization library using cava"; + } diff --git a/lib/greet/config.vala.in b/lib/greet/config.vala.in index 333d735..a7990aa 100644 --- a/lib/greet/config.vala.in +++ b/lib/greet/config.vala.in @@ -1,3 +1,4 @@ +[CCode (gir_namespace = "AstalGreet", gir_version = "@API_VERSION@")] namespace AstalGreet { public const int MAJOR_VERSION = @MAJOR_VERSION@; public const int MINOR_VERSION = @MINOR_VERSION@; diff --git a/lib/greet/default.nix b/lib/greet/default.nix new file mode 100644 index 0000000..22b9456 --- /dev/null +++ b/lib/greet/default.nix @@ -0,0 +1,15 @@ +{ + mkAstalPkg, + pkgs, + ... +}: +mkAstalPkg { + pname = "astal-greet"; + src = ./.; + packages = [pkgs.json-glib]; + + libname = "greet"; + authors = "Aylur"; + gir-suffix = "Greet"; + description = "IPC client for greetd"; +} diff --git a/lib/greet/meson.build b/lib/greet/meson.build index 11321b0..8bd35d7 100644 --- a/lib/greet/meson.build +++ b/lib/greet/meson.build @@ -25,6 +25,7 @@ config = configure_file( input: 'config.vala.in', output: 'config.vala', configuration: { + 'API_VERSION': api_version, 'VERSION': meson.project_version(), 'MAJOR_VERSION': version_split[0], 'MINOR_VERSION': version_split[1], diff --git a/lib/hyprland/config.vala.in b/lib/hyprland/config.vala.in index 65993b2..0cdba75 100644 --- a/lib/hyprland/config.vala.in +++ b/lib/hyprland/config.vala.in @@ -1,3 +1,4 @@ +[CCode (gir_namespace = "AstalHyprland", gir_version = "@API_VERSION@")] namespace AstalHyprland { public const int MAJOR_VERSION = @MAJOR_VERSION@; public const int MINOR_VERSION = @MINOR_VERSION@; diff --git a/lib/hyprland/default.nix b/lib/hyprland/default.nix new file mode 100644 index 0000000..6b5e15d --- /dev/null +++ b/lib/hyprland/default.nix @@ -0,0 +1,15 @@ +{ + mkAstalPkg, + pkgs, + ... +}: +mkAstalPkg { + pname = "astal-hyprland"; + src = ./.; + packages = [pkgs.json-glib]; + + libname = "hyprland"; + authors = "Aylur"; + gir-suffix = "Hyprland"; + description = "IPC client for Hyprland"; +} diff --git a/lib/hyprland/hyprland.vala b/lib/hyprland/hyprland.vala index 9825a91..70b5a25 100644 --- a/lib/hyprland/hyprland.vala +++ b/lib/hyprland/hyprland.vala @@ -303,6 +303,20 @@ public class Hyprland : Object { } } + private async bool try_add_client(string addr) throws Error { + if (addr == "" || get_client(addr) != null) { + return true; + } + + var client = new Client(); + _clients.insert(addr, client); + yield sync_clients(); + yield sync_workspaces(); + client_added(client); + notify_property("clients"); + return false; + } + private async void handle_event(string line) throws Error { var args = line.split(">>"); @@ -315,25 +329,11 @@ public class Hyprland : Object { case "focusedmon": var argv = args[1].split(",", 2); + yield sync_monitors(); focused_monitor = get_monitor_by_name(argv[0]); focused_workspace = get_workspace_by_name(argv[1]); break; - // first event that signals a new client - case "activewindowv2": - if (args[1] != "" && get_client(args[1]) == null) { - var client = new Client(); - _clients.insert(args[1], client); - yield sync_clients(); - yield sync_workspaces(); - client_added(client); - notify_property("clients"); - focused_client = client; - } else { - focused_client = get_client(args[1]); - } - break; - // TODO: nag vaxry for fullscreenv2 that passes address case "fullscreen": yield sync_clients(); @@ -392,9 +392,18 @@ public class Hyprland : Object { keyboard_layout(argv[0], argv[1]); break; + // first event that signals a new client when it opens as an active window + case "activewindowv2": + yield try_add_client(args[1]); + focused_client = get_client(args[1]); + break; + case "openwindow": - yield sync_clients(); - yield sync_workspaces(); + var addr = args[1].split(",")[0]; + if (yield try_add_client(addr)) { + yield sync_clients(); + yield sync_workspaces(); + } break; case "closewindow": diff --git a/lib/hyprland/meson.build b/lib/hyprland/meson.build index 7112ee1..9598ba9 100644 --- a/lib/hyprland/meson.build +++ b/lib/hyprland/meson.build @@ -25,6 +25,7 @@ config = configure_file( input: 'config.vala.in', output: 'config.vala', configuration: { + 'API_VERSION': api_version, 'VERSION': meson.project_version(), 'MAJOR_VERSION': version_split[0], 'MINOR_VERSION': version_split[1], diff --git a/lib/hyprland/structs.vala b/lib/hyprland/structs.vala index 25f70c3..d5180b1 100644 --- a/lib/hyprland/structs.vala +++ b/lib/hyprland/structs.vala @@ -4,12 +4,15 @@ public class Bind : Object { public bool mouse { get; construct set; } public bool release { get; construct set; } public bool repeat { get; construct set; } + public bool long_press { get; construct set; } public bool non_consuming { get; construct set; } + public bool has_description { get; construct set; } public int64 modmask { get; construct set; } public string submap { get; construct set; } public string key { get; construct set; } public int64 keycode { get; construct set; } public bool catch_all { get; construct set; } + public string description { get; construct set; } public string dispatcher { get; construct set; } public string arg { get; construct set; } @@ -18,12 +21,15 @@ public class Bind : Object { mouse = obj.get_boolean_member("mouse"); release = obj.get_boolean_member("release"); repeat = obj.get_boolean_member("repeat"); + long_press = obj.get_member("longPress")?.get_boolean() ?? false; non_consuming = obj.get_boolean_member("non_consuming"); + has_description = obj.get_member("has_description")?.get_boolean() ?? false; modmask = obj.get_int_member("modmask"); submap = obj.get_string_member("submap"); key = obj.get_string_member("key"); keycode = obj.get_int_member("keycode"); catch_all = obj.get_boolean_member("catch_all"); + description = obj.get_member("description")?.get_string() ?? ""; dispatcher = obj.get_string_member("dispatcher"); arg = obj.get_string_member("arg"); } diff --git a/lib/mpris/config.vala.in b/lib/mpris/config.vala.in index 767c4bd..f44c3dc 100644 --- a/lib/mpris/config.vala.in +++ b/lib/mpris/config.vala.in @@ -1,3 +1,4 @@ +[CCode (gir_namespace = "AstalMpris", gir_version = "@API_VERSION@")] namespace AstalMpris { public const int MAJOR_VERSION = @MAJOR_VERSION@; public const int MINOR_VERSION = @MINOR_VERSION@; diff --git a/lib/mpris/default.nix b/lib/mpris/default.nix new file mode 100644 index 0000000..082c647 --- /dev/null +++ b/lib/mpris/default.nix @@ -0,0 +1,15 @@ +{ + mkAstalPkg, + pkgs, + ... +}: +mkAstalPkg { + pname = "astal-mpris"; + src = ./.; + packages = with pkgs; [gvfs json-glib]; + + libname = "mpris"; + authors = "Aylur"; + gir-suffix = "Mpris"; + description = "Control mpris players"; +} diff --git a/lib/mpris/meson.build b/lib/mpris/meson.build index bf215c9..91185c8 100644 --- a/lib/mpris/meson.build +++ b/lib/mpris/meson.build @@ -25,6 +25,7 @@ config = configure_file( input: 'config.vala.in', output: 'config.vala', configuration: { + 'API_VERSION': api_version, 'VERSION': meson.project_version(), 'MAJOR_VERSION': version_split[0], 'MINOR_VERSION': version_split[1], diff --git a/lib/mpris/player.vala b/lib/mpris/player.vala index c69eb21..e6d84bf 100644 --- a/lib/mpris/player.vala +++ b/lib/mpris/player.vala @@ -440,6 +440,9 @@ public class AstalMpris.Player : Object { _loop_status = Loop.from_string(proxy.loop_status); notify_property("loop-status"); } + } else { + _loop_status = Loop.UNSUPPORTED; + notify_property("loop-status"); } if (rate != proxy.rate) { @@ -452,6 +455,9 @@ public class AstalMpris.Player : Object { _shuffle_status = Shuffle.from_bool(proxy.shuffle); notify_property("shuffle-status"); } + } else { + _shuffle_status = Shuffle.UNSUPPORTED; + notify_property("shuffle-status"); } if (volume != proxy.volume) { @@ -498,8 +504,10 @@ public class AstalMpris.Player : Object { } private async void cache_cover() { - if (art_url == null || art_url == "") + if (art_url == null || art_url == "") { + cover_art = null; return; + } var file = File.new_for_uri(art_url); if (file.get_path() != null) { diff --git a/lib/network/config.vala.in b/lib/network/config.vala.in index dbec0f3..3e5beb5 100644 --- a/lib/network/config.vala.in +++ b/lib/network/config.vala.in @@ -1,3 +1,4 @@ +[CCode (gir_namespace = "AstalNetwork", gir_version = "@API_VERSION@")] namespace AstalNetwork { public const int MAJOR_VERSION = @MAJOR_VERSION@; public const int MINOR_VERSION = @MINOR_VERSION@; diff --git a/lib/network/default.nix b/lib/network/default.nix new file mode 100644 index 0000000..cfa2378 --- /dev/null +++ b/lib/network/default.nix @@ -0,0 +1,16 @@ +{ + mkAstalPkg, + pkgs, + ... +}: +mkAstalPkg { + pname = "astal-network"; + src = ./.; + packages = [pkgs.networkmanager]; + + libname = "network"; + authors = "Aylur"; + gir-suffix = "Network"; + description = "NetworkManager wrapper library"; + dependencies = ["NM-1.0"]; +} diff --git a/lib/network/meson.build b/lib/network/meson.build index 17ea358..a490179 100644 --- a/lib/network/meson.build +++ b/lib/network/meson.build @@ -20,6 +20,7 @@ config = configure_file( input: 'config.vala.in', output: 'config.vala', configuration: { + 'API_VERSION': api_version, 'VERSION': meson.project_version(), 'MAJOR_VERSION': version_split[0], 'MINOR_VERSION': version_split[1], diff --git a/lib/network/network.vala b/lib/network/network.vala index 96e19c8..b5a6c61 100644 --- a/lib/network/network.vala +++ b/lib/network/network.vala @@ -104,14 +104,14 @@ public enum AstalNetwork.Primary { // alias for NM.State public enum AstalNetwork.State { - UNKNOWN, - ASLEEP, - DISCONNECTED, - DISCONNECTING, - CONNECTING, - CONNECTED_LOCAL, - CONNECTED_SITE, - CONNECTED_GLOBAL; + UNKNOWN = 0, + ASLEEP = 10, + DISCONNECTED = 20, + DISCONNECTING = 30, + CONNECTING = 40, + CONNECTED_LOCAL = 50, + CONNECTED_SITE = 60, + CONNECTED_GLOBAL = 70; public string to_string() { switch (this) { diff --git a/lib/notifd/config.vala.in b/lib/notifd/config.vala.in index 752c754..04b2cd2 100644 --- a/lib/notifd/config.vala.in +++ b/lib/notifd/config.vala.in @@ -1,3 +1,4 @@ +[CCode (gir_namespace = "AstalNotifd", gir_version = "@API_VERSION@")] namespace AstalNotifd { public const int MAJOR_VERSION = @MAJOR_VERSION@; public const int MINOR_VERSION = @MINOR_VERSION@; diff --git a/lib/notifd/default.nix b/lib/notifd/default.nix new file mode 100644 index 0000000..e23528c --- /dev/null +++ b/lib/notifd/default.nix @@ -0,0 +1,15 @@ +{ + mkAstalPkg, + pkgs, + ... +}: +mkAstalPkg { + pname = "astal-notifd"; + src = ./.; + packages = with pkgs; [json-glib gdk-pixbuf]; + + libname = "notifd"; + authors = "Aylur"; + gir-suffix = "Notifd"; + description = "Notification daemon library"; +} diff --git a/lib/notifd/meson.build b/lib/notifd/meson.build index 3d4de95..a9e7faf 100644 --- a/lib/notifd/meson.build +++ b/lib/notifd/meson.build @@ -26,6 +26,7 @@ config = configure_file( input: 'config.vala.in', output: 'config.vala', configuration: { + 'API_VERSION': api_version, 'VERSION': meson.project_version(), 'MAJOR_VERSION': version_split[0], 'MINOR_VERSION': version_split[1], diff --git a/lib/powerprofiles/cli.vala b/lib/powerprofiles/cli.vala index 1e5cc70..87ffe82 100644 --- a/lib/powerprofiles/cli.vala +++ b/lib/powerprofiles/cli.vala @@ -100,7 +100,7 @@ string to_json_string(AstalPowerProfiles.PowerProfiles profiles) { foreach (var prof in profiles.profiles) { profs.add_value(new Json.Builder() .begin_object() - .set_member_name("profie").add_string_value(prof.profile) + .set_member_name("profile").add_string_value(prof.profile) .set_member_name("driver").add_string_value(prof.driver) .set_member_name("cpu_driver").add_string_value(prof.cpu_driver) .set_member_name("platform_driver").add_string_value(prof.platform_driver) diff --git a/lib/powerprofiles/config.vala.in b/lib/powerprofiles/config.vala.in index 79034f1..cc9d309 100644 --- a/lib/powerprofiles/config.vala.in +++ b/lib/powerprofiles/config.vala.in @@ -1,3 +1,4 @@ +[CCode (gir_namespace = "AstalPowerProfiles", gir_version = "@API_VERSION@")] namespace AstalPowerProfiles { public const int MAJOR_VERSION = @MAJOR_VERSION@; public const int MINOR_VERSION = @MINOR_VERSION@; diff --git a/lib/powerprofiles/default.nix b/lib/powerprofiles/default.nix new file mode 100644 index 0000000..f35f9e5 --- /dev/null +++ b/lib/powerprofiles/default.nix @@ -0,0 +1,15 @@ +{ + mkAstalPkg, + pkgs, + ... +}: +mkAstalPkg { + pname = "astal-powerprofiles"; + src = ./.; + packages = [pkgs.json-glib]; + + libname = "powerprofiles"; + authors = "Aylur"; + gir-suffix = "PowerProfiles"; + description = "DBus proxy for upowerd profiles"; +} diff --git a/lib/powerprofiles/meson.build b/lib/powerprofiles/meson.build index cd8cc2b..4a31afa 100644 --- a/lib/powerprofiles/meson.build +++ b/lib/powerprofiles/meson.build @@ -25,6 +25,7 @@ config = configure_file( input: 'config.vala.in', output: 'config.vala', configuration: { + 'API_VERSION': api_version, 'VERSION': meson.project_version(), 'MAJOR_VERSION': version_split[0], 'MINOR_VERSION': version_split[1], diff --git a/lib/powerprofiles/power-profiles.vala b/lib/powerprofiles/power-profiles.vala index a104d2e..931fc04 100644 --- a/lib/powerprofiles/power-profiles.vala +++ b/lib/powerprofiles/power-profiles.vala @@ -113,6 +113,11 @@ public class PowerProfiles : Object { owned get { return proxy.performance_degraded; } } + private string? get_hashtable_string(HashTable<string, Variant> table, string key) { + var v = table.get(key); + return v == null ? null : v.get_string(); + } + /** * List of each profile. */ @@ -122,10 +127,10 @@ public class PowerProfiles : Object { for (var i = 0; i < proxy.profiles.length; ++i) { var prof = proxy.profiles[i]; profs[i] = Profile() { - profile = prof.get("Profile").get_string(), - cpu_driver = prof.get("CpuDriver").get_string(), - platform_driver = prof.get("PlatformDriver").get_string(), - driver = prof.get("Driver").get_string() + profile = get_hashtable_string(prof, "Profile"), + cpu_driver = get_hashtable_string(prof, "CpuDriver"), + platform_driver = get_hashtable_string(prof, "PlatformDriver"), + driver = get_hashtable_string(prof, "Driver"), }; } return profs; diff --git a/lib/river/default.nix b/lib/river/default.nix new file mode 100644 index 0000000..15c6eb4 --- /dev/null +++ b/lib/river/default.nix @@ -0,0 +1,21 @@ +{ + mkAstalPkg, + pkgs, + ... +}: +mkAstalPkg { + pname = "astal-river"; + src = ./.; + packages = [pkgs.json-glib]; + + libname = "river"; + authors = "kotontrion"; + gir-suffix = "River"; + description = "IPC client for River"; + + postUnpack = '' + rm -rf $sourceRoot/subprojects + mkdir -p $sourceRoot/subprojects + cp -r --remove-destination ${../wayland-glib} $sourceRoot/subprojects/wayland-glib + ''; +} diff --git a/lib/river/include/wayland-source.h b/lib/river/include/wayland-source.h deleted file mode 100644 index b219589..0000000 --- a/lib/river/include/wayland-source.h +++ /dev/null @@ -1,16 +0,0 @@ -#ifndef __WAYLAND_SOURCE_H__ -#define __WAYLAND_SOURCE_H__ - -#include <glib-object.h> - -G_BEGIN_DECLS - -typedef struct _WLSource WLSource; - -WLSource* wl_source_new(); -void wl_source_free(WLSource* self); -struct wl_display* wl_source_get_display(WLSource* source); - -G_END_DECLS - -#endif /* __WAYLAND_SOURCE_H__ */ diff --git a/lib/river/meson.build b/lib/river/meson.build index aa5e23b..2f98315 100644 --- a/lib/river/meson.build +++ b/lib/river/meson.build @@ -13,6 +13,9 @@ lib_so_version = version_split[0] + '.' + version_split[1] pkg_config = import('pkgconfig') gnome = import('gnome') +wayland_glib = subproject('wayland-glib') +wayland_glib_dep = wayland_glib.get_variable('wayland_glib') + subdir('protocols') subdir('include') subdir('src') diff --git a/lib/river/src/meson.build b/lib/river/src/meson.build index b7ce20d..9c58e41 100644 --- a/lib/river/src/meson.build +++ b/lib/river/src/meson.build @@ -1,7 +1,6 @@ srcs = files( 'river-output.c', - 'river.c', - 'wayland-source.c', + 'river.c' ) deps = [ @@ -9,6 +8,7 @@ deps = [ dependency('gio-2.0'), dependency('wayland-client'), dependency('json-glib-1.0'), + wayland_glib_dep ] astal_river_lib = library( diff --git a/lib/river/src/river.c b/lib/river/src/river.c index 124cb20..6f94c9c 100644 --- a/lib/river/src/river.c +++ b/lib/river/src/river.c @@ -6,7 +6,8 @@ #include "river-control-unstable-v1-client.h" #include "river-private.h" #include "river-status-unstable-v1-client.h" -#include "wayland-source.h" +// #include "wayland-source.h" +#include <wayland-glib.h> struct _AstalRiverRiver { GObject parent_instance; @@ -22,7 +23,7 @@ typedef struct { struct wl_registry* wl_registry; struct wl_seat* seat; struct wl_display* display; - WLSource* wl_source; + WlGlibWlSource* wl_source; struct zriver_status_manager_v1* river_status_manager; struct zriver_control_v1* river_control; struct zriver_seat_status_v1* river_seat_status; @@ -340,7 +341,7 @@ static gboolean astal_river_river_initable_init(GInitable* initable, GCancellabl if (priv->init) return TRUE; - priv->wl_source = wl_source_new(NULL, NULL); + priv->wl_source = wl_glib_wl_source_new(); if (priv->wl_source == NULL) { g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_FAILED, @@ -348,7 +349,7 @@ static gboolean astal_river_river_initable_init(GInitable* initable, GCancellabl return FALSE; } - priv->display = wl_source_get_display(priv->wl_source); + priv->display = priv->wl_source->display; priv->wl_registry = wl_display_get_registry(priv->display); wl_registry_add_listener(priv->wl_registry, ®istry_listener, self); @@ -449,7 +450,8 @@ static void astal_river_river_finalize(GObject* object) { if (priv->seat != NULL) wl_seat_destroy(priv->seat); if (priv->display != NULL) wl_display_flush(priv->display); - if (priv->wl_source != NULL) wl_source_free(priv->wl_source); + // if (priv->wl_source != NULL) wl_source_free(priv->wl_source); + if (priv->wl_source != NULL) g_source_unref((GSource*)(priv->wl_source)); g_free(self->focused_view); g_free(self->focused_output); diff --git a/lib/river/src/wayland-source.c b/lib/river/src/wayland-source.c deleted file mode 100644 index 875c32c..0000000 --- a/lib/river/src/wayland-source.c +++ /dev/null @@ -1,104 +0,0 @@ - -#include "wayland-source.h" - -#include <errno.h> -#include <glib.h> -#include <wayland-client.h> - -struct _WLSource { - GSource source; - struct wl_display *display; - gpointer fd; - int error; -}; - -static gboolean wl_source_prepare(GSource *source, gint *timeout) { - WLSource *self = (WLSource *)source; - - *timeout = 0; - if (wl_display_prepare_read(self->display) != 0) - return TRUE; - else if (wl_display_flush(self->display) < 0) { - self->error = errno; - return TRUE; - } - *timeout = -1; - return FALSE; -} - -static gboolean wl_source_check(GSource *source) { - WLSource *self = (WLSource *)source; - - if (self->error > 0) return TRUE; - - GIOCondition revents; - revents = g_source_query_unix_fd(source, self->fd); - - if (revents & G_IO_IN) { - if (wl_display_read_events(self->display) < 0) self->error = errno; - } else - wl_display_cancel_read(self->display); - - return revents > 0; -} - -static gboolean wl_source_dispatch(GSource *source, GSourceFunc callback, gpointer user_data) { - WLSource *self = (WLSource *)source; - GIOCondition revents; - - revents = g_source_query_unix_fd(source, self->fd); - if ((self->error > 0) || (revents & (G_IO_ERR | G_IO_HUP))) { - errno = self->error; - self->error = 0; - if (callback != NULL) return callback(user_data); - return G_SOURCE_REMOVE; - } - - if (wl_display_dispatch_pending(self->display) < 0) { - if (callback != NULL) return callback(user_data); - return G_SOURCE_REMOVE; - } - - return G_SOURCE_CONTINUE; -} - -static void wl_source_finalize(GSource *source) { - WLSource *self = (WLSource *)source; - wl_display_disconnect(self->display); -} - -static GSourceFuncs wl_source_funcs = { - .prepare = wl_source_prepare, - .check = wl_source_check, - .dispatch = wl_source_dispatch, - .finalize = wl_source_finalize, -}; - -WLSource *wl_source_new() { - struct wl_display *display; - WLSource *self; - GSource *source; - - display = wl_display_connect(NULL); - if (display == NULL) return NULL; - - source = g_source_new(&wl_source_funcs, sizeof(WLSource)); - self = (WLSource *)source; - self->display = display; - - self->fd = g_source_add_unix_fd(source, wl_display_get_fd(self->display), - G_IO_IN | G_IO_ERR | G_IO_HUP); - - g_source_attach(source, NULL); - - return self; -} - -void wl_source_free(WLSource *self) { - GSource *source = (GSource *)self; - g_return_if_fail(source != NULL); - g_source_destroy(source); - g_source_unref(source); -} - -struct wl_display *wl_source_get_display(WLSource *self) { return self->display; } diff --git a/lib/river/subprojects/wayland-glib b/lib/river/subprojects/wayland-glib new file mode 120000 index 0000000..41372cf --- /dev/null +++ b/lib/river/subprojects/wayland-glib @@ -0,0 +1 @@ +../../wayland-glib
\ No newline at end of file diff --git a/lib/tray/config.vala.in b/lib/tray/config.vala.in index 8ef8498..6a46bec 100644 --- a/lib/tray/config.vala.in +++ b/lib/tray/config.vala.in @@ -1,3 +1,4 @@ +[CCode (gir_namespace = "AstalTray", gir_version = "@API_VERSION@")] namespace AstalTray { public const int MAJOR_VERSION = @MAJOR_VERSION@; public const int MINOR_VERSION = @MINOR_VERSION@; diff --git a/lib/tray/default.nix b/lib/tray/default.nix new file mode 100644 index 0000000..6dd0d61 --- /dev/null +++ b/lib/tray/default.nix @@ -0,0 +1,41 @@ +{ + mkAstalPkg, + pkgs, + ... +}: let + vala-panel-appmenu = pkgs.fetchFromGitLab { + owner = "vala-panel-project"; + repo = "vala-panel-appmenu"; + rev = "24.05"; + hash = "sha256-8GWauw7r3zKhvGF2TNOI8GDVctUFDhtG/Vy1cNUpsVo="; + }; + + appmenu-glib-translator = pkgs.stdenv.mkDerivation { + pname = "appmenu-glib-translator"; + version = "24.05"; + + src = "${vala-panel-appmenu}/subprojects/appmenu-glib-translator"; + + buildInputs = with pkgs; [ + glib + ]; + + nativeBuildInputs = with pkgs; [ + gobject-introspection + meson + pkg-config + ninja + vala + ]; + }; +in + mkAstalPkg { + pname = "astal-tray"; + src = ./.; + packages = [pkgs.json-glib appmenu-glib-translator]; + + libname = "tray"; + authors = "kotontrion"; + gir-suffix = "Tray"; + description = "StatusNotifierItem implementation"; + } diff --git a/lib/tray/meson.build b/lib/tray/meson.build index fbf2f98..4fca2e4 100644 --- a/lib/tray/meson.build +++ b/lib/tray/meson.build @@ -26,6 +26,7 @@ config = configure_file( input: 'config.vala.in', output: 'config.vala', configuration: { + 'API_VERSION': api_version, 'VERSION': meson.project_version(), 'MAJOR_VERSION': version_split[0], 'MINOR_VERSION': version_split[1], @@ -39,29 +40,9 @@ deps = [ dependency('gio-2.0'), dependency('json-glib-1.0'), dependency('gdk-pixbuf-2.0'), - dependency('gtk+-3.0'), + dependency('appmenu-glib-translator') ] -dbusmenu_cflags = run_command( - find_program('pkg-config', required: true), - '--cflags', 'dbusmenu-gtk3-0.4', - 'gobject-introspection-1.0', - 'gobject-2.0', - 'glib-2.0', - capture: true, - check: true, -).stdout().strip() - -dbusmenu_libs = run_command( - find_program('pkg-config', required: true), - '--libs', 'dbusmenu-gtk3-0.4', - 'gobject-introspection-1.0', - 'gobject-2.0', - 'glib-2.0', - capture: true, - check: true, -).stdout().strip() - sources = [config] + files('tray.vala', 'watcher.vala', 'trayItem.vala') if get_option('lib') @@ -71,15 +52,13 @@ if get_option('lib') dependencies: deps, vala_header: meson.project_name() + '.h', vala_vapi: meson.project_name() + '-' + api_version + '.vapi', - vala_args: ['--vapi-comments', '--pkg', 'DbusmenuGtk3-0.4', '--pkg', 'Dbusmenu-0.4'], + vala_args: ['--vapi-comments'], version: meson.project_version(), - c_args: dbusmenu_cflags.split(' '), - link_args: dbusmenu_libs.split(' '), install: true, install_dir: [true, true, true] ) - pkgs = ['--pkg', 'DbusmenuGtk3-0.4', '--pkg', 'Dbusmenu-0.4'] + pkgs = [] foreach dep : deps pkgs += ['--pkg=' + dep.name()] endforeach @@ -120,31 +99,13 @@ if get_option('lib') requires: deps, install_dir: get_option('libdir') / 'pkgconfig', ) - # - # custom_target( - # typelib, - # command: [ - # find_program('g-ir-compiler'), - # '--output', '@OUTPUT@', - # '--shared-library', get_option('prefix') / get_option('libdir') / '@PLAINNAME@', - # meson.current_build_dir() / gir, - # ], - # input: lib, - # output: typelib, - # depends: lib, - # install: true, - # install_dir: get_option('libdir') / 'girepository-1.0', - # ) -endif + endif if get_option('cli') executable( meson.project_name(), ['cli.vala', sources], dependencies: deps, - vala_args: ['--pkg', 'DbusmenuGtk3-0.4', '--pkg', 'Dbusmenu-0.4'], - c_args: dbusmenu_cflags.split(' '), - link_args: dbusmenu_libs.split(' '), install: true, ) endif diff --git a/lib/tray/trayItem.vala b/lib/tray/trayItem.vala index db0e6d4..6da51cb 100644 --- a/lib/tray/trayItem.vala +++ b/lib/tray/trayItem.vala @@ -1,5 +1,3 @@ -using DbusmenuGtk; - namespace AstalTray { public struct Pixmap { int width; @@ -88,21 +86,20 @@ public class TrayItem : Object { private IItem proxy; private List<ulong> connection_ids; - /** The Title of the TrayItem */ public string title { owned get { return proxy.Title; } } - + /** The category this item belongs to */ public Category category { get { return proxy.Category; } } - - /** the current status of this item */ + + /** The current status of this item */ public Status status { get { return proxy.Status; } } - - /** the tooltip of this item */ + + /** The tooltip of this item */ public Tooltip? tooltip { owned get { return proxy.ToolTip; } } - - /** - * a markup representation of the tooltip. This is basically equvivalent + + /** + * A markup representation of the tooltip. This is basically equvivalent * to `tooltip.title \n tooltip.description` */ public string tooltip_markup { @@ -120,26 +117,24 @@ public class TrayItem : Object { /** the id of the item. This id is specified by the tray app.*/ public string id { owned get { return proxy.Id ;} } - - /** - * If set, this only supports the menu, so showing the menu should be prefered + + /** + * If set, this only supports the menu, so showing the menu should be prefered * over calling [[email protected]]. */ public bool is_menu { get { return proxy.ItemIsMenu ;} } - - /** - * the icon theme path, where to look for the [[email protected]:icon-name]. - * - * It is recommended to use the [[email protected]:gicon] property, + + /** + * The icon theme path, where to look for the [[email protected]:icon-name]. + * It is recommended to use the [[email protected]:gicon] property, * which does the icon lookups for you. */ public string icon_theme_path { owned get { return proxy.IconThemePath ;} } - - /** - * the name of the icon. This should be looked up in the [[email protected]:icon-theme-path] - * if set or in the currently used icon theme otherwise. - * - * It is recommended to use the [[email protected]:gicon] property, + + /** + * The name of the icon. This should be looked up in the [[email protected]:icon-theme-path] + * if set or in the currently used icon theme otherwise. + * It is recommended to use the [[email protected]:gicon] property, * which does the icon lookups for you. */ public string icon_name { @@ -149,26 +144,41 @@ public class TrayItem : Object { : proxy.IconName; } } - - /** - * a pixbuf containing the icon. - * - * It is recommended to use the [[email protected]:gicon] property, + + /** + * A pixbuf containing the icon. + * It is recommended to use the [[email protected]:gicon] property, * which does the icon lookups for you. */ public Gdk.Pixbuf icon_pixbuf { owned get { return _get_icon_pixbuf(); } } /** - * contains the items icon. This property is intended to be used with the gicon property + * Contains the items icon. This property is intended to be used with the gicon property * of the Icon widget and the recommended way to display the icon. - * This property unifies the [[email protected]:icon-name], + * This property unifies the [[email protected]:icon-name], * [[email protected]:icon-theme-path] and [[email protected]:icon-pixbuf] properties. */ public GLib.Icon gicon { get; private set; } - - /** the id of the item used to uniquely identify the TrayItems by this lib.*/ + + /** The id of the item used to uniquely identify the TrayItems by this lib.*/ public string item_id { get; private set; } + private DBusMenu.Importer menu_importer; + + public MenuModel? menu_model { + owned get { + if (menu_importer == null) return null; + return menu_importer.model; + } + } + + public ActionGroup? action_group { + owned get { + if (menu_importer == null) return null; + return menu_importer.action_group; + } + } + public signal void changed(); public signal void ready(); @@ -183,7 +193,8 @@ public class TrayItem : Object { proxy = yield Bus.get_proxy( BusType.SESSION, service, - path); + path + ); connection_ids.append(proxy.NewStatus.connect(refresh_all_properties)); connection_ids.append(proxy.NewToolTip.connect(refresh_all_properties)); @@ -196,9 +207,16 @@ public class TrayItem : Object { SignalHandler.disconnect(proxy, id); } }); - - update_gicon(); + if (proxy.Menu != null) { + this.menu_importer = new DBusMenu.Importer(proxy.get_name_owner(), proxy.Menu); + this.menu_importer.notify["model"].connect(() => { + notify_property("menu-model"); + notify_property("action-group"); + }); + } + + update_gicon(); ready(); } catch (Error err) { critical(err.message); @@ -206,28 +224,33 @@ public class TrayItem : Object { } private void _notify() { - string[] props = { "category", "id", "title", "status", "is-menu", "tooltip-markup", "icon-name", "icon-pixbuf" }; - - foreach (string prop in props) + string[] props = { + "category", + "id", + "title", + "status", + "is-menu", + "tooltip-markup", + "icon-name", + "icon-pixbuf" + }; + + foreach (string prop in props) { notify_property(prop); + } changed(); } private void update_gicon() { - if(icon_name != null && icon_name != "") { - if(icon_theme_path != null && icon_theme_path != "") { - - Gtk.IconTheme icon_theme = new Gtk.IconTheme(); - string[] paths = {icon_theme_path}; - icon_theme.set_search_path(paths); - - int size = icon_theme.get_icon_sizes(icon_name)[0]; - Gtk.IconInfo icon_info = icon_theme.lookup_icon( - icon_name, size, Gtk.IconLookupFlags.FORCE_SIZE); - - if (icon_info != null) - gicon = new GLib.FileIcon(GLib.File.new_for_path(icon_info.get_filename())); + if (icon_name != null && icon_name != "") { + if (GLib.FileUtils.test(icon_name, GLib.FileTest.EXISTS)) { + gicon = new GLib.FileIcon(GLib.File.new_for_path(icon_name)); + } + else if(icon_theme_path != null && icon_theme_path != "") { + gicon = new GLib.FileIcon(GLib.File.new_for_path( + find_icon_in_theme(icon_name, icon_theme_path) + )); } else { gicon = new GLib.ThemedIcon(icon_name); } @@ -274,33 +297,60 @@ public class TrayItem : Object { } ); } - + + /** + * tells the tray app that its menu is about to be opened, + * so it can update the menu if needed. You should call this method + * before openening the menu. + */ + public void about_to_show() { + if(proxy.Menu == null) return; + try { + Bus.get_sync(BusType.SESSION).call_sync( + this.proxy.g_name_owner, + this.proxy.Menu, + "com.canonical.dbusmenu", + "AboutToShow", + new Variant("(i)", 0), + null, + DBusCallFlags.NONE, + -1, + null + ); + } + catch (Error r) { + //silently ignore + } + } + /** - * send an activate request to the tray app. + * Send an activate request to the tray app. */ public void activate(int x, int y) { try { proxy.Activate(x, y); } catch (Error e) { - if(e.domain != DBusError.quark() || e.code != DBusError.UNKNOWN_METHOD) + if (e.domain != DBusError.quark() || e.code != DBusError.UNKNOWN_METHOD) { warning(e.message); + } } } /** - * send a secondary activate request to the tray app. + * Send a secondary activate request to the tray app. */ public void secondary_activate(int x, int y) { try { proxy.SecondaryActivate(x, y); } catch (Error e) { - if(e.domain != DBusError.quark() || e.code != DBusError.UNKNOWN_METHOD) + if (e.domain != DBusError.quark() || e.code != DBusError.UNKNOWN_METHOD) { warning(e.message); + } } } /** - * send a scroll request to the tray app. + * Send a scroll request to the tray app. * valid values for the orientation are "horizontal" and "vertical". */ public void scroll(int delta, string orientation) { @@ -312,58 +362,44 @@ public class TrayItem : Object { } } - /** - * creates a new Gtk Menu for this item. - */ - public Gtk.Menu? create_menu() { - if (proxy.Menu == null) + private string? find_icon_in_theme(string icon_name, string theme_path){ + if (icon_name == null || theme_path == null || icon_name == "" || theme_path == "") { return null; + } - return new DbusmenuGtk.Menu( - proxy.get_name_owner(), - proxy.Menu); - } - - private Gdk.Pixbuf? _get_icon_pixbuf() { - Pixmap[] pixmaps = proxy.Status == Status.NEEDS_ATTENTION - ? proxy.AttentionIconPixmap - : proxy.IconPixmap; - - - string icon_name = proxy.Status == Status.NEEDS_ATTENTION - ? proxy.AttentionIconName - : proxy.IconName; - - Gdk.Pixbuf pixbuf = null; - - if (icon_name != null && proxy.IconThemePath != null) - pixbuf = load_from_theme(icon_name, proxy.IconThemePath); + try { + Dir dir = Dir.open(theme_path, 0); + string? name = null; - if (pixbuf == null) - pixbuf = pixmap_to_pixbuf(pixmaps); + while ((name = dir.read_name ()) != null) { + var path = Path.build_filename(theme_path, name); - return pixbuf; - } + if (FileUtils.test(path, FileTest.IS_DIR)) { + string? icon = find_icon_in_theme(icon_name, path); + if (icon != null) return icon; + else continue; + } - private Gdk.Pixbuf? load_from_theme(string icon_name, string theme_path) { - if (theme_path == "" || theme_path == null) - return null; + int dot_index = name.last_index_of("."); + if (dot_index != -1) { + name = name.substring(0, dot_index); + } - if (icon_name == "" || icon_name == null) + if (name == icon_name) return path; + } + } catch (FileError err) { return null; + } + return null; - Gtk.IconTheme icon_theme = new Gtk.IconTheme(); - string[] paths = {theme_path}; - icon_theme.set_search_path(paths); - - int size = icon_theme.get_icon_sizes(icon_name)[0]; - Gtk.IconInfo icon_info = icon_theme.lookup_icon( - icon_name, size, Gtk.IconLookupFlags.FORCE_SIZE); + } - if (icon_info != null) - return icon_info.load_icon(); + private Gdk.Pixbuf? _get_icon_pixbuf() { + Pixmap[] pixmaps = proxy.Status == Status.NEEDS_ATTENTION + ? proxy.AttentionIconPixmap + : proxy.IconPixmap; - return null; + return pixmap_to_pixbuf(pixmaps); } private Gdk.Pixbuf? pixmap_to_pixbuf(Pixmap[] pixmaps) { @@ -371,6 +407,12 @@ public class TrayItem : Object { return null; Pixmap pixmap = pixmaps[0]; + + for (int i = 0; i < pixmaps.length; i++){ + if(pixmap.width < pixmaps[i].width) + pixmap = pixmaps[i]; + }; + uint8[] image_data = pixmap.bytes.copy(); for (int i = 0; i < pixmap.width * pixmap.height * 4; i += 4) { diff --git a/lib/wayland-glib/meson.build b/lib/wayland-glib/meson.build new file mode 100644 index 0000000..3d93ac0 --- /dev/null +++ b/lib/wayland-glib/meson.build @@ -0,0 +1,40 @@ +project( + 'wayland-glib', + 'vala', + 'c', + version: run_command('cat', join_paths(meson.project_source_root(), 'version')).stdout().strip(), + meson_version: '>= 0.62.0', + default_options: [ + 'warning_level=2', + 'werror=false', + 'c_std=gnu11', + ], +) + +version_split = meson.project_version().split('.') + +deps = [ + dependency('glib-2.0'), + dependency('gio-2.0'), + dependency('gobject-2.0'), + dependency('wayland-client'), +] + +sources = [ + 'wl-source.vala', +] + +lib = static_library( + meson.project_name(), + sources, + dependencies: deps, + vala_header: meson.project_name() + '.h', + vala_vapi: meson.project_name() + '.vapi', +) + +wayland_glib = declare_dependency( + link_with: lib, + include_directories: include_directories('.') + ) + + diff --git a/lib/wayland-glib/version b/lib/wayland-glib/version new file mode 100644 index 0000000..6e8bf73 --- /dev/null +++ b/lib/wayland-glib/version @@ -0,0 +1 @@ +0.1.0 diff --git a/lib/wayland-glib/wl-source.vala b/lib/wayland-glib/wl-source.vala new file mode 100644 index 0000000..f8472fb --- /dev/null +++ b/lib/wayland-glib/wl-source.vala @@ -0,0 +1,44 @@ +namespace WlGlib { +public class WlSource : Source { + + public Wl.Display display; + public void* fd; + public int error; + + public override bool dispatch(SourceFunc callback) { + IOCondition revents = this.query_unix_fd(this.fd); + if (this.error > 0 || (revents & (IOCondition.ERR | IOCondition.HUP)) != 0) { + errno = this.error; + if(callback != null) return callback(); + return Source.REMOVE; + } + if (((revents & IOCondition.IN) != 0) && this.display.dispatch() < 0) { + if(callback != null) return callback(); + return Source.REMOVE; + } + return Source.CONTINUE; + } + + public override bool check() { + IOCondition revents = this.query_unix_fd(this.fd); + return revents > 0; + } + + public override bool prepare(out int timeout) { + if(this.display.flush() < 0) + this.error = errno; + timeout = -1; + return false; + } + + public WlSource() { + base(); + this.display = new Wl.Display.connect(null); + if(this.display == null) return; + this.fd = this.add_unix_fd(this.display.get_fd(), + IOCondition.IN | IOCondition.ERR | IOCondition.HUP); + this.attach(null); + } +} +} + diff --git a/lib/wireplumber/default.nix b/lib/wireplumber/default.nix new file mode 100644 index 0000000..f9eb832 --- /dev/null +++ b/lib/wireplumber/default.nix @@ -0,0 +1,16 @@ +{ + mkAstalPkg, + pkgs, + ... +}: +mkAstalPkg { + pname = "astal-wireplumber"; + src = ./.; + packages = [pkgs.wireplumber]; + + libname = "wireplumber"; + authors = "kotontrion"; + gir-suffix = "Wp"; + description = "Wrapper library over the wireplumber API"; + dependencies = ["WP-0.5"]; +} diff --git a/nix/devshell.nix b/nix/devshell.nix index 66c46e5..2e409c4 100644 --- a/nix/devshell.nix +++ b/nix/devshell.nix @@ -40,19 +40,31 @@ libdbusmenu-gtk3 wayland blueprint-compiler + libadwaita + wayland-scanner dart-sass lua python gjs ]; + + lsp = with pkgs; [ + nodejs + mesonlsp + vala-language-server + vtsls + vscode-langservers-extracted + markdownlint-cli2 + ]; in { default = pkgs.mkShell { - inherit buildInputs; + packages = buildInputs ++ lsp; }; astal = pkgs.mkShell { - buildInputs = + packages = buildInputs + ++ lsp ++ builtins.attrValues ( builtins.removeAttrs self.packages.${pkgs.system} ["docs"] ); diff --git a/nix/doc/gi-docgen.patch b/nix/gi-docgen.patch index 0ecb4bd..0ecb4bd 100644 --- a/nix/doc/gi-docgen.patch +++ b/nix/gi-docgen.patch diff --git a/nix/libcava.nix b/nix/libcava.nix deleted file mode 100644 index 866599d..0000000 --- a/nix/libcava.nix +++ /dev/null @@ -1,60 +0,0 @@ -{ - stdenv, - fetchFromGitHub, - autoreconfHook, - autoconf-archive, - alsa-lib, - fftw, - iniparser, - libpulseaudio, - portaudio, - sndio, - SDL2, - libGL, - pipewire, - jack2, - ncurses, - pkgconf, - meson, - ninja, -}: -stdenv.mkDerivation rec { - pname = "cava"; - version = "0.10.3"; - - src = fetchFromGitHub { - owner = "LukashonakV"; - repo = "cava"; - rev = "0.10.3"; - hash = "sha256-ZDFbI69ECsUTjbhlw2kHRufZbQMu+FQSMmncCJ5pagg="; - }; - - buildInputs = [ - alsa-lib - libpulseaudio - ncurses - iniparser - sndio - SDL2 - libGL - portaudio - jack2 - pipewire - ]; - - propagatedBuildInputs = [ - fftw - ]; - - nativeBuildInputs = [ - autoreconfHook - autoconf-archive - pkgconf - meson - ninja - ]; - - preAutoreconf = '' - echo ${version} > version - ''; -} diff --git a/nix/lua.nix b/nix/lua.nix index d4221f1..fc68fdd 100644 --- a/nix/lua.nix +++ b/nix/lua.nix @@ -1,6 +1,6 @@ -defaults: { - pkgs ? defaults.pkgs, - astal ? defaults.astal, +self: { + pkgs, + astal ? self, name ? "astal-lua", src, extraLuaPackages ? (ps: []), diff --git a/nix/mkAstalPkg.nix b/nix/mkAstalPkg.nix new file mode 100644 index 0000000..b0a00af --- /dev/null +++ b/nix/mkAstalPkg.nix @@ -0,0 +1,150 @@ +pkgs: let + inherit (builtins) elem elemAt readFile replaceStrings splitVersion toJSON; + inherit (pkgs.lib) filterAttrs; + + readVer = file: replaceStrings ["\n"] [""] (readFile file); + + toTOML = (pkgs.formats.toml {}).generate; + + docgen = pkgs.gi-docgen.overrideAttrs { + patches = [./gi-docgen.patch]; + }; + + dependency = { + "GObject-2.0" = { + name = "GObject"; + description = "The base type system library"; + docs_url = "https://docs.gtk.org/gobject/"; + }; + "Gtk-3.0" = { + name = "Gtk"; + description = "The GTK toolkit"; + docs_url = "https://docs.gtk.org/gtk3/"; + }; + "Gtk-4.0" = { + name = "Gtk"; + description = "The GTK toolkit"; + docs_url = "https://docs.gtk.org/gtk4/"; + }; + "AstalIO-0.1" = { + name = "AstalIO"; + description = "Astal Core library"; + docs_url = "https://aylur.github.io/libastal/io"; + }; + "NM-1.0" = { + name = "NetworkManager"; + description = "The standard Linux network configuration tool suite"; + docs_url = "https://networkmanager.dev/docs/libnm/latest/"; + }; + "WP-0.5" = { + name = "WirePlumber"; + description = "Modular session/policy manager for PipeWire"; + docs_url = "https://pipewire.pages.freedesktop.org/wireplumber/"; + }; + }; + + urlmap = pkgs.writeText "urlmap" '' + baseURLs = ${toJSON [ + ["GLib" "https://docs.gtk.org/glib/"] + ["GObject" "https://docs.gtk.org/gobject/"] + ["Gio" "https://docs.gtk.org/gio/"] + ["Gdk" "https://docs.gtk.org/gdk3/"] + ["Gtk" "https://docs.gtk.org/gtk3/"] + ["GdkPixbuf" "https://docs.gtk.org/gdk-pixbuf/"] + ["AstalIO" "https://aylur.github.io/libastal/io"] + + # FIXME: these are not gi-docgen generated, therefore links are broken + ["NM" "https://networkmanager.dev/docs/libnm/latest/"] + ["WP" "https://pipewire.pages.freedesktop.org/wireplumber/"] + ]} + ''; +in + { + src, + pname, + libname, + gir-suffix, + authors, + description, + dependencies ? [], + repo-path ? libname, + website-path ? libname, + nativeBuildInputs ? [], + packages ? [], + postUnpack ? "", + }: let + version = readVer "${src}/version"; + + ver = splitVersion version; + api-ver = "${elemAt ver 0}.${elemAt ver 1}"; + girName = "Astal${gir-suffix}-${api-ver}"; + in + pkgs.stdenv.mkDerivation { + inherit pname src version; + outputs = ["out" "dev" "doc"]; + + nativeBuildInputs = with pkgs; + [ + wrapGAppsHook + gobject-introspection + meson + pkg-config + ninja + vala + wayland + wayland-scanner + python3 + ] + ++ nativeBuildInputs; + + propagatedBuildInputs = with pkgs; + [ + glib + ] + ++ packages; + + postUnpack = '' + cp --remove-destination ${../lib/gir.py} $sourceRoot/gir.py + ${postUnpack} + ''; + + postInstall = let + data = toTOML libname { + library = { + inherit description authors version; + license = "LGPL-2.1"; + browse_url = "https://github.com/Aylur/astal/tree/main/lib/${repo-path}"; + repository_url = "https://github.com/aylur/aylur.git"; + website_url = "https://aylur.github.io/astal/guide/libraries/${website-path}"; + dependencies = ["GObject-2.0"] ++ dependencies; + }; + + extra.urlmap_file = "urlmap.js"; + dependencies = + {inherit (dependency) "GObject-2.0";} + // (filterAttrs (n: _: elem n dependencies) dependency); + }; + in '' + gir="${girName}.gir" + + mkdir -p $out/share/doc/${website-path} + cat ${urlmap} > urlmap.js + + if [ -d "src" ]; then + gir="src/$gir" + fi + + ${docgen}/bin/gi-docgen generate --config ${data} $gir + mv ${girName}/* $out/share/doc/${website-path} + ''; + + passthru = { + inherit girName; + }; + + meta = { + inherit description; + homepage = "https://aylur.github.io/astal"; + license = pkgs.lib.licenses.lgpl21; + }; + } |