summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/gi-docs.yml3
-rw-r--r--CONTRIBUTING.md2
-rw-r--r--docs/default.nix240
-rw-r--r--docs/guide/getting-started/installation.md8
-rw-r--r--docs/guide/getting-started/introduction.md12
-rw-r--r--docs/guide/getting-started/nix.md38
-rw-r--r--docs/guide/getting-started/supported-languages.md23
-rw-r--r--docs/guide/libraries/apps.md4
-rw-r--r--docs/guide/libraries/battery.md4
-rw-r--r--docs/guide/libraries/bluetooth.md4
-rw-r--r--docs/guide/libraries/greet.md4
-rw-r--r--docs/guide/libraries/hyprland.md4
-rw-r--r--docs/guide/libraries/mpris.md4
-rw-r--r--docs/guide/libraries/network.md4
-rw-r--r--docs/guide/libraries/notifd.md4
-rw-r--r--docs/guide/libraries/powerprofiles.md4
-rw-r--r--docs/guide/libraries/tray.md19
-rw-r--r--docs/guide/libraries/wireplumber.md2
-rw-r--r--docs/guide/lua/binding.md21
-rw-r--r--docs/guide/lua/examples.md11
-rw-r--r--docs/guide/typescript/cli-app.md6
-rw-r--r--docs/guide/typescript/examples.md11
-rw-r--r--docs/guide/typescript/faq.md42
-rw-r--r--docs/guide/typescript/first-widgets.md24
-rw-r--r--docs/guide/typescript/theming.md17
-rw-r--r--docs/guide/typescript/widget.md381
-rw-r--r--docs/package.json2
-rw-r--r--docs/public/showcase/aylur.pngbin3202086 -> 0 bytes
-rw-r--r--docs/public/showcase/aylur.webpbin0 -> 59972 bytes
-rw-r--r--docs/public/showcase/ezerinz.webpbin0 -> 191114 bytes
-rw-r--r--docs/public/showcase/hyprpanel_showcase.webpbin0 -> 87210 bytes
-rw-r--r--docs/public/showcase/tokyob0t.webp (renamed from docs/public/showcase/tokyob0t-super-duper-hiper-mega-ultra-contribution.webp)bin122412 -> 122412 bytes
-rw-r--r--docs/public/vitepress-logo-large.webpbin33980 -> 0 bytes
-rw-r--r--docs/showcases/showcases.ts20
-rw-r--r--examples/gtk3/js/.gitignore (renamed from examples/js/.gitignore)0
-rw-r--r--examples/gtk3/js/applauncher/README.md (renamed from examples/js/applauncher/README.md)0
-rw-r--r--examples/gtk3/js/applauncher/app.ts (renamed from examples/js/applauncher/app.ts)0
-rw-r--r--examples/gtk3/js/applauncher/style.scss (renamed from examples/js/applauncher/style.scss)0
-rw-r--r--examples/gtk3/js/applauncher/widget/Applauncher.scss (renamed from examples/js/applauncher/widget/Applauncher.scss)0
-rw-r--r--examples/gtk3/js/applauncher/widget/Applauncher.tsx (renamed from examples/js/applauncher/widget/Applauncher.tsx)10
-rw-r--r--examples/gtk3/js/media-player/README.md (renamed from examples/js/media-player/README.md)0
-rw-r--r--examples/gtk3/js/media-player/app.ts (renamed from examples/js/media-player/app.ts)0
-rw-r--r--examples/gtk3/js/media-player/style.scss (renamed from examples/js/media-player/style.scss)0
-rw-r--r--examples/gtk3/js/media-player/widget/MediaPlayer.scss (renamed from examples/js/media-player/widget/MediaPlayer.scss)0
-rw-r--r--examples/gtk3/js/media-player/widget/MediaPlayer.tsx (renamed from examples/js/media-player/widget/MediaPlayer.tsx)0
-rw-r--r--examples/gtk3/js/notifications/README.md (renamed from examples/js/notifications/README.md)0
-rw-r--r--examples/gtk3/js/notifications/app.ts (renamed from examples/js/notifications/app.ts)0
-rw-r--r--examples/gtk3/js/notifications/notifications/Notification.scss (renamed from examples/js/notifications/notifications/Notification.scss)0
-rw-r--r--examples/gtk3/js/notifications/notifications/Notification.tsx (renamed from examples/js/notifications/notifications/Notification.tsx)0
-rw-r--r--examples/gtk3/js/notifications/notifications/NotificationPopups.tsx (renamed from examples/js/notifications/notifications/NotificationPopups.tsx)2
-rw-r--r--examples/gtk3/js/notifications/style.scss (renamed from examples/js/notifications/style.scss)0
-rw-r--r--examples/gtk3/js/osd/README.md7
-rw-r--r--examples/gtk3/js/osd/app.ts11
-rw-r--r--examples/gtk3/js/osd/osd/OSD.scss30
-rw-r--r--examples/gtk3/js/osd/osd/OSD.tsx69
-rw-r--r--examples/gtk3/js/osd/osd/brightness.ts45
-rw-r--r--examples/gtk3/js/osd/style.scss1
-rw-r--r--examples/gtk3/js/popover/Popover.tsx90
-rw-r--r--examples/gtk3/js/popover/Popover2.tsx66
-rw-r--r--examples/gtk3/js/popover/app.tsx72
-rw-r--r--examples/gtk3/js/simple-bar/README.md (renamed from examples/js/simple-bar/README.md)0
-rw-r--r--examples/gtk3/js/simple-bar/app.ts (renamed from examples/js/simple-bar/app.ts)0
-rw-r--r--examples/gtk3/js/simple-bar/style.scss (renamed from examples/vala/simple-bar/style.scss)50
-rw-r--r--examples/gtk3/js/simple-bar/widget/Bar.tsx (renamed from examples/js/simple-bar/widget/Bar.tsx)43
-rw-r--r--examples/gtk3/lua/applauncher/README.md5
-rw-r--r--examples/gtk3/lua/applauncher/init.lua16
-rw-r--r--examples/gtk3/lua/applauncher/lib.lua38
-rw-r--r--examples/gtk3/lua/applauncher/style.scss1
-rw-r--r--examples/gtk3/lua/applauncher/widget/Applauncher.lua118
-rw-r--r--examples/gtk3/lua/applauncher/widget/Applauncher.scss59
-rw-r--r--examples/gtk3/lua/media-player/README.md5
-rw-r--r--examples/gtk3/lua/media-player/init.lua19
-rw-r--r--examples/gtk3/lua/media-player/lib.lua38
-rw-r--r--examples/gtk3/lua/media-player/style.scss1
-rw-r--r--examples/gtk3/lua/media-player/widget/MediaPlayer.lua144
-rw-r--r--examples/gtk3/lua/media-player/widget/MediaPlayer.scss56
-rw-r--r--examples/gtk3/lua/notifications/README.md5
-rw-r--r--examples/gtk3/lua/notifications/init.lua20
-rw-r--r--examples/gtk3/lua/notifications/lib.lua74
-rw-r--r--examples/gtk3/lua/notifications/notifications/Notification.lua105
-rw-r--r--examples/gtk3/lua/notifications/notifications/Notification.scss126
-rw-r--r--examples/gtk3/lua/notifications/notifications/NotificationPopups.lua57
-rw-r--r--examples/gtk3/lua/notifications/style.scss1
-rw-r--r--examples/gtk3/lua/simple-bar/README.md (renamed from examples/lua/simple-bar/README.md)0
-rw-r--r--examples/gtk3/lua/simple-bar/init.lua (renamed from examples/lua/simple-bar/init.lua)0
-rw-r--r--examples/gtk3/lua/simple-bar/lib.lua (renamed from examples/lua/simple-bar/lib.lua)8
-rw-r--r--examples/gtk3/lua/simple-bar/style.scss (renamed from examples/lua/simple-bar/style.scss)50
-rw-r--r--examples/gtk3/lua/simple-bar/widget/Bar.lua (renamed from examples/lua/simple-bar/widget/Bar.lua)135
-rw-r--r--examples/gtk3/lua/stylua.toml4
-rw-r--r--examples/gtk3/py/.gitignore (renamed from examples/py/.gitignore)0
-rw-r--r--examples/gtk3/py/simple-bar/README.md (renamed from examples/py/simple-bar/README.md)0
-rw-r--r--examples/gtk3/py/simple-bar/__init__.py (renamed from examples/py/simple-bar/__init__.py)0
-rwxr-xr-xexamples/gtk3/py/simple-bar/app.py (renamed from examples/py/simple-bar/app.py)6
-rw-r--r--examples/gtk3/py/simple-bar/style.scss (renamed from examples/js/simple-bar/style.scss)50
-rw-r--r--examples/gtk3/py/simple-bar/versions.py (renamed from examples/py/simple-bar/versions.py)0
-rw-r--r--examples/gtk3/py/simple-bar/widget/Bar.py (renamed from examples/py/simple-bar/widget/Bar.py)38
-rw-r--r--examples/gtk3/py/simple-bar/widget/__init__.py (renamed from examples/py/simple-bar/widget/__init__.py)0
-rw-r--r--examples/gtk3/vala/simple-bar/README.md (renamed from examples/vala/simple-bar/README.md)0
-rw-r--r--examples/gtk3/vala/simple-bar/app.in.vala (renamed from examples/vala/simple-bar/app.in.vala)0
-rw-r--r--examples/gtk3/vala/simple-bar/flake.nix (renamed from examples/vala/simple-bar/flake.nix)0
-rw-r--r--examples/gtk3/vala/simple-bar/meson.build (renamed from examples/vala/simple-bar/meson.build)19
-rw-r--r--examples/gtk3/vala/simple-bar/style.scss (renamed from examples/py/simple-bar/style.scss)50
-rw-r--r--examples/gtk3/vala/simple-bar/widget/Bar.vala (renamed from examples/vala/simple-bar/widget/Bar.vala)40
-rw-r--r--flake.lock6
-rw-r--r--flake.nix103
-rw-r--r--lang/gjs/default.nix16
-rw-r--r--lang/gjs/eslint.config.mjs1
-rw-r--r--lang/gjs/meson.build14
-rw-r--r--lang/gjs/package.json8
-rw-r--r--lang/gjs/src/_app.ts9
-rw-r--r--lang/gjs/src/_astal.ts193
-rw-r--r--lang/gjs/src/binding.ts6
-rw-r--r--lang/gjs/src/file.ts6
-rw-r--r--lang/gjs/src/gobject.ts14
-rw-r--r--lang/gjs/src/gtk3/astalify.ts232
-rw-r--r--lang/gjs/src/gtk3/index.ts1
-rw-r--r--lang/gjs/src/gtk3/jsx-runtime.ts33
-rw-r--r--lang/gjs/src/gtk3/widget.ts27
-rw-r--r--lang/gjs/src/gtk4/app.ts11
-rw-r--r--lang/gjs/src/gtk4/astalify.ts228
-rw-r--r--lang/gjs/src/gtk4/index.ts7
-rw-r--r--lang/gjs/src/gtk4/jsx-runtime.ts69
-rw-r--r--lang/gjs/src/gtk4/widget.ts167
-rw-r--r--lang/gjs/src/index.ts4
-rw-r--r--lang/gjs/src/overrides.ts5
-rw-r--r--lang/gjs/src/package.json22
-rw-r--r--lang/gjs/src/process.ts12
-rw-r--r--lang/gjs/src/time.ts3
-rw-r--r--lang/gjs/src/variable.ts13
-rw-r--r--lang/lua/astal/gtk3/astalify.lua25
-rw-r--r--lang/lua/astal/gtk3/widget.lua7
-rw-r--r--lang/lua/astal/variable.lua4
-rw-r--r--lib/apps/default.nix15
-rw-r--r--lib/apps/fuzzy.vala6
-rw-r--r--lib/astal/gtk3/default.nix21
-rw-r--r--lib/astal/gtk3/src/config.vala.in1
-rw-r--r--lib/astal/gtk3/src/meson.build1
-rw-r--r--lib/astal/gtk3/src/widget/circularprogress.vala2
-rw-r--r--lib/astal/gtk3/src/widget/icon.vala19
-rw-r--r--lib/astal/gtk3/src/widget/slider.vala12
-rw-r--r--lib/astal/gtk3/src/widget/window.vala9
-rw-r--r--lib/astal/gtk4/default.nix21
-rw-r--r--lib/astal/gtk4/src/application.vala20
-rw-r--r--lib/astal/gtk4/src/config.vala.in1
-rw-r--r--lib/astal/gtk4/src/meson.build3
-rw-r--r--lib/astal/gtk4/src/widget/box.vala50
-rw-r--r--lib/astal/gtk4/src/widget/slider.vala77
-rw-r--r--lib/astal/gtk4/src/widget/window.vala7
-rw-r--r--lib/astal/io/config.vala.in1
-rw-r--r--lib/astal/io/daemon.vala88
-rw-r--r--lib/astal/io/default.nix12
-rw-r--r--lib/astal/io/file.vala10
-rw-r--r--lib/astal/io/meson.build2
-rw-r--r--lib/astal/io/process.vala89
-rw-r--r--lib/astal/io/variable.vala2
-rw-r--r--lib/auth/default.nix15
-rw-r--r--lib/battery/config.vala.in1
-rw-r--r--lib/battery/default.nix15
-rw-r--r--lib/battery/meson.build1
-rw-r--r--lib/bluetooth/config.vala.in1
-rw-r--r--lib/bluetooth/default.nix10
-rw-r--r--lib/bluetooth/meson.build1
-rw-r--r--lib/cava/cava.c4
-rw-r--r--lib/cava/default.nix56
-rw-r--r--lib/greet/config.vala.in1
-rw-r--r--lib/greet/default.nix15
-rw-r--r--lib/greet/meson.build1
-rw-r--r--lib/hyprland/config.vala.in1
-rw-r--r--lib/hyprland/default.nix15
-rw-r--r--lib/hyprland/hyprland.vala43
-rw-r--r--lib/hyprland/meson.build1
-rw-r--r--lib/hyprland/structs.vala6
-rw-r--r--lib/mpris/config.vala.in1
-rw-r--r--lib/mpris/default.nix15
-rw-r--r--lib/mpris/meson.build1
-rw-r--r--lib/mpris/player.vala10
-rw-r--r--lib/network/config.vala.in1
-rw-r--r--lib/network/default.nix16
-rw-r--r--lib/network/meson.build1
-rw-r--r--lib/network/network.vala16
-rw-r--r--lib/notifd/config.vala.in1
-rw-r--r--lib/notifd/default.nix15
-rw-r--r--lib/notifd/meson.build1
-rw-r--r--lib/powerprofiles/cli.vala2
-rw-r--r--lib/powerprofiles/config.vala.in1
-rw-r--r--lib/powerprofiles/default.nix15
-rw-r--r--lib/powerprofiles/meson.build1
-rw-r--r--lib/powerprofiles/power-profiles.vala13
-rw-r--r--lib/river/default.nix21
-rw-r--r--lib/river/include/wayland-source.h16
-rw-r--r--lib/river/meson.build3
-rw-r--r--lib/river/src/meson.build4
-rw-r--r--lib/river/src/river.c12
-rw-r--r--lib/river/src/wayland-source.c104
l---------lib/river/subprojects/wayland-glib1
-rw-r--r--lib/tray/config.vala.in1
-rw-r--r--lib/tray/default.nix41
-rw-r--r--lib/tray/meson.build49
-rw-r--r--lib/tray/trayItem.vala244
-rw-r--r--lib/wayland-glib/meson.build40
-rw-r--r--lib/wayland-glib/version1
-rw-r--r--lib/wayland-glib/wl-source.vala44
-rw-r--r--lib/wireplumber/default.nix16
-rw-r--r--nix/devshell.nix16
-rw-r--r--nix/gi-docgen.patch (renamed from nix/doc/gi-docgen.patch)0
-rw-r--r--nix/libcava.nix60
-rw-r--r--nix/lua.nix6
-rw-r--r--nix/mkAstalPkg.nix150
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)
![simple-bar](https://github.com/user-attachments/assets/a306c864-56b7-44c4-8820-81f424f32b9b)
-- [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)
![notification-popups](https://github.com/user-attachments/assets/0df0eddc-5c74-4af0-a694-48dc8ec6bb44)
-- [Applauncher](https://github.com/Aylur/astal/tree/main/examples/js/applauncher)
+- [Applauncher](https://github.com/Aylur/astal/tree/main/examples/gtk3/js/applauncher)
![launcher](https://github.com/user-attachments/assets/2695e3bb-dff4-478a-b392-279fe638bfd3)
-- [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)
![media-player](https://github.com/user-attachments/assets/891e9706-74db-4505-bd83-c3628d7b4fd0)
## 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)
![simple-bar](https://github.com/user-attachments/assets/a306c864-56b7-44c4-8820-81f424f32b9b)
+- [Notification Popups](https://github.com/Aylur/astal/tree/main/examples/lua/notifications)
+![notification-popups](https://github.com/user-attachments/assets/0df0eddc-5c74-4af0-a694-48dc8ec6bb44)
+
+- [Applauncher](https://github.com/Aylur/astal/tree/main/examples/lua/applauncher)
+![launcher](https://github.com/user-attachments/assets/2695e3bb-dff4-478a-b392-279fe638bfd3)
+
+- [Media Player](https://github.com/Aylur/astal/tree/main/examples/lua/media-player)
+![media-player](https://github.com/user-attachments/assets/891e9706-74db-4505-bd83-c3628d7b4fd0)
+
## 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)
![simple-bar](https://github.com/user-attachments/assets/a306c864-56b7-44c4-8820-81f424f32b9b)
## 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)
![simple-bar](https://github.com/user-attachments/assets/a306c864-56b7-44c4-8820-81f424f32b9b)
## 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)
![simple-bar](https://github.com/user-attachments/assets/a306c864-56b7-44c4-8820-81f424f32b9b)
+
+### [Notification Popups](https://github.com/Aylur/astal/tree/main/examples/gtk3/lua/notifications)
+![notification-popups](https://github.com/user-attachments/assets/0df0eddc-5c74-4af0-a694-48dc8ec6bb44)
+
+### [Applauncher](https://github.com/Aylur/astal/tree/main/examples/gtk3/lua/applauncher)
+![launcher](https://github.com/user-attachments/assets/2695e3bb-dff4-478a-b392-279fe638bfd3)
+
+### [Media Player](https://github.com/Aylur/astal/tree/main/examples/gtk3/lua/media-player)
+![media-player](https://github.com/user-attachments/assets/891e9706-74db-4505-bd83-c3628d7b4fd0)
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)
![simple-bar](https://github.com/user-attachments/assets/a306c864-56b7-44c4-8820-81f424f32b9b)
-### [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)
![notification-popups](https://github.com/user-attachments/assets/0df0eddc-5c74-4af0-a694-48dc8ec6bb44)
-### [Applauncher](https://github.com/Aylur/astal/tree/main/examples/js/applauncher)
+### [Applauncher](https://github.com/Aylur/astal/tree/main/examples/gtk3/js/applauncher)
![launcher](https://github.com/user-attachments/assets/2695e3bb-dff4-478a-b392-279fe638bfd3)
-### [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)
![media-player](https://github.com/user-attachments/assets/891e9706-74db-4505-bd83-c3628d7b4fd0)
+
+### [OSD](https://github.com/Aylur/astal/tree/main/examples/gtk3/js/osd)
+![osd](https://github.com/user-attachments/assets/08e0e118-6b07-4cac-8ebc-08262594cee7)
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
deleted file mode 100644
index 5dc5dd7..0000000
--- a/docs/public/showcase/aylur.png
+++ /dev/null
Binary files differ
diff --git a/docs/public/showcase/aylur.webp b/docs/public/showcase/aylur.webp
new file mode 100644
index 0000000..7f89e04
--- /dev/null
+++ b/docs/public/showcase/aylur.webp
Binary files differ
diff --git a/docs/public/showcase/ezerinz.webp b/docs/public/showcase/ezerinz.webp
new file mode 100644
index 0000000..7da8d7d
--- /dev/null
+++ b/docs/public/showcase/ezerinz.webp
Binary files differ
diff --git a/docs/public/showcase/hyprpanel_showcase.webp b/docs/public/showcase/hyprpanel_showcase.webp
new file mode 100644
index 0000000..6a9ad7c
--- /dev/null
+++ b/docs/public/showcase/hyprpanel_showcase.webp
Binary files differ
diff --git a/docs/public/showcase/tokyob0t-super-duper-hiper-mega-ultra-contribution.webp b/docs/public/showcase/tokyob0t.webp
index 94f6080..94f6080 100644
--- a/docs/public/showcase/tokyob0t-super-duper-hiper-mega-ultra-contribution.webp
+++ b/docs/public/showcase/tokyob0t.webp
Binary files differ
diff --git a/docs/public/vitepress-logo-large.webp b/docs/public/vitepress-logo-large.webp
deleted file mode 100644
index 2e47b15..0000000
--- a/docs/public/vitepress-logo-large.webp
+++ /dev/null
Binary files differ
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
+
+![osd](https://github.com/user-attachments/assets/08e0e118-6b07-4cac-8ebc-08262594cee7)
+
+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
+
+![launcher](https://github.com/user-attachments/assets/2695e3bb-dff4-478a-b392-279fe638bfd3)
+
+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
+
+![mpris](https://github.com/user-attachments/assets/891e9706-74db-4505-bd83-c3628d7b4fd0)
+
+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
+
+![notifs](https://github.com/user-attachments/assets/0df0eddc-5c74-4af0-a694-48dc8ec6bb44)
+
+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);
+ }
}
}
diff --git a/flake.lock b/flake.lock
index eec56ea..fea8e52 100644
--- a/flake.lock
+++ b/flake.lock
@@ -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": {
diff --git a/flake.nix b/flake.nix
index d7c5229..c18de7b 100644
--- a/flake.nix
+++ b/flake.nix
@@ -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, &registry_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;
+ };
+ }