summaryrefslogtreecommitdiff
path: root/examples
diff options
context:
space:
mode:
Diffstat (limited to 'examples')
-rw-r--r--examples/gtk4/simple-bar/js/.gitignore5
-rw-r--r--examples/gtk4/simple-bar/js/README.md53
-rw-r--r--examples/gtk4/simple-bar/js/flake.nix52
-rw-r--r--examples/gtk4/simple-bar/js/meson.build12
-rw-r--r--examples/gtk4/simple-bar/js/src/gresource.xml8
-rwxr-xr-xexamples/gtk4/simple-bar/js/src/main.in.js12
-rwxr-xr-xexamples/gtk4/simple-bar/js/src/main.in.sh3
-rw-r--r--examples/gtk4/simple-bar/js/src/main.scss1
-rw-r--r--examples/gtk4/simple-bar/js/src/meson.build84
-rw-r--r--examples/gtk4/simple-bar/js/src/scss/Bar.scss19
-rw-r--r--examples/gtk4/simple-bar/js/src/ts/App.ts52
-rw-r--r--examples/gtk4/simple-bar/js/src/ts/Bar.ts189
-rw-r--r--examples/gtk4/simple-bar/js/src/ts/props.ts45
-rw-r--r--examples/gtk4/simple-bar/js/src/ui/Bar.blp84
-rw-r--r--examples/gtk4/simple-bar/js/tsconfig.json8
-rw-r--r--examples/gtk4/simple-bar/py/.gitignore3
-rw-r--r--examples/gtk4/simple-bar/py/README.md45
-rw-r--r--examples/gtk4/simple-bar/py/flake.nix55
-rw-r--r--examples/gtk4/simple-bar/py/meson.build12
-rw-r--r--examples/gtk4/simple-bar/py/src/gresource.xml7
-rwxr-xr-xexamples/gtk4/simple-bar/py/src/main.in.py19
-rw-r--r--examples/gtk4/simple-bar/py/src/main.scss1
-rw-r--r--examples/gtk4/simple-bar/py/src/meson.build59
-rw-r--r--examples/gtk4/simple-bar/py/src/py/App.py41
-rw-r--r--examples/gtk4/simple-bar/py/src/py/Bar.py184
-rw-r--r--examples/gtk4/simple-bar/py/src/py/__init__.py15
-rw-r--r--examples/gtk4/simple-bar/py/src/scss/Bar.scss19
-rw-r--r--examples/gtk4/simple-bar/py/src/ui/Bar.blp84
-rw-r--r--examples/gtk4/simple-bar/vala/.gitignore1
-rw-r--r--examples/gtk4/simple-bar/vala/README.md44
-rw-r--r--examples/gtk4/simple-bar/vala/flake.nix52
-rw-r--r--examples/gtk4/simple-bar/vala/meson.build3
-rw-r--r--examples/gtk4/simple-bar/vala/src/gresource.xml7
-rw-r--r--examples/gtk4/simple-bar/vala/src/main.scss1
-rw-r--r--examples/gtk4/simple-bar/vala/src/meson.build67
-rw-r--r--examples/gtk4/simple-bar/vala/src/scss/Bar.scss19
-rw-r--r--examples/gtk4/simple-bar/vala/src/ui/Bar.blp84
-rw-r--r--examples/gtk4/simple-bar/vala/src/vala/App.vala45
-rw-r--r--examples/gtk4/simple-bar/vala/src/vala/Bar.vala186
39 files changed, 1680 insertions, 0 deletions
diff --git a/examples/gtk4/simple-bar/js/.gitignore b/examples/gtk4/simple-bar/js/.gitignore
new file mode 100644
index 0000000..2b7806f
--- /dev/null
+++ b/examples/gtk4/simple-bar/js/.gitignore
@@ -0,0 +1,5 @@
+node_modules/
+build/
+result
+result/
+@types/
diff --git a/examples/gtk4/simple-bar/js/README.md b/examples/gtk4/simple-bar/js/README.md
new file mode 100644
index 0000000..6719fdb
--- /dev/null
+++ b/examples/gtk4/simple-bar/js/README.md
@@ -0,0 +1,53 @@
+# Simple Astal Bar example in TypeScript
+
+This example shows you how to get a TypeScript+Blueprint+Sass project going.
+
+## Dependencies
+
+- gjs
+- meson
+- esbuild
+- blueprint-compiler
+- sass
+- astal4
+- astal-battery
+- astal-wireplumber
+- astak-network
+- astal-mpris
+- astak-power-profiles
+- astal-tray
+- astal-bluetooth
+
+## How to use
+
+> [!NOTE]
+> If you are on Nix, there is an example flake included
+> otherwise feel free to `rm flake.nix`
+
+- generate types with `ts-for-gir`
+
+ ```sh
+ # might take a while
+ # also, don't worry about warning and error logs
+ npx @ts-for-gir/cli generate --ignoreVersionConflicts
+ ```
+
+- developing
+
+ ```sh
+ meson setup build --wipe --prefix "$pwd/result"
+ meson install -C build
+ ./result/bin/simple-bar
+ ```
+
+- installing
+
+ ```sh
+ meson setup build --wipe --prefix /usr
+ meson install -C build
+ simple-bar
+ ```
+
+- adding new typescript files requires no additional steps
+- adding new scss files requires no additional steps as long as they are imported from `main.scss`
+- adding new ui (blueprint) files will also have to be listed in `meson.build` and in `gresource.xml`
diff --git a/examples/gtk4/simple-bar/js/flake.nix b/examples/gtk4/simple-bar/js/flake.nix
new file mode 100644
index 0000000..b626880
--- /dev/null
+++ b/examples/gtk4/simple-bar/js/flake.nix
@@ -0,0 +1,52 @@
+{
+ inputs = {
+ nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
+ astal = {
+ url = "github:aylur/astal";
+ inputs.nixpkgs.follows = "nixpkgs";
+ };
+ };
+
+ outputs = {
+ self,
+ nixpkgs,
+ astal,
+ }: let
+ system = "x86_64-linux";
+ pkgs = nixpkgs.legacyPackages.${system};
+
+ nativeBuildInputs = with pkgs; [
+ meson
+ ninja
+ pkg-config
+ gobject-introspection
+ wrapGAppsHook4
+ blueprint-compiler
+ dart-sass
+ esbuild
+ ];
+
+ astalPackages = with astal.packages.${system}; [
+ io
+ astal4
+ battery
+ wireplumber
+ network
+ mpris
+ powerprofiles
+ tray
+ bluetooth
+ ];
+ in {
+ packages.${system}.default = pkgs.stdenv.mkDerivation {
+ name = "simple-bar";
+ src = ./.;
+ inherit nativeBuildInputs;
+ buildInputs = astalPackages ++ [pkgs.gjs];
+ };
+
+ devShells.${system}.default = pkgs.mkShell {
+ packages = nativeBuildInputs ++ astalPackages ++ [pkgs.gjs];
+ };
+ };
+}
diff --git a/examples/gtk4/simple-bar/js/meson.build b/examples/gtk4/simple-bar/js/meson.build
new file mode 100644
index 0000000..f49af2e
--- /dev/null
+++ b/examples/gtk4/simple-bar/js/meson.build
@@ -0,0 +1,12 @@
+project('simple-bar')
+
+dependency('astal-4-4.0')
+dependency('astal-battery-0.1')
+dependency('astal-wireplumber-0.1')
+dependency('astal-network-0.1')
+dependency('astal-mpris-0.1')
+dependency('astal-power-profiles-0.1')
+dependency('astal-tray-0.1')
+dependency('astal-bluetooth-0.1')
+
+subdir('src')
diff --git a/examples/gtk4/simple-bar/js/src/gresource.xml b/examples/gtk4/simple-bar/js/src/gresource.xml
new file mode 100644
index 0000000..0960d6a
--- /dev/null
+++ b/examples/gtk4/simple-bar/js/src/gresource.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/">
+ <file>index.js</file>
+ <file>main.css</file>
+ <file>ui/Bar.ui</file>
+ </gresource>
+</gresources>
diff --git a/examples/gtk4/simple-bar/js/src/main.in.js b/examples/gtk4/simple-bar/js/src/main.in.js
new file mode 100755
index 0000000..90e8f99
--- /dev/null
+++ b/examples/gtk4/simple-bar/js/src/main.in.js
@@ -0,0 +1,12 @@
+#!@GJS@ -m
+
+import { exit, programArgs } from "system"
+import Gio from "gi://Gio"
+import GLib from "gi://GLib"
+
+// makes sure `LD_PRELOAD` does not leak into subprocesses
+GLib.setenv("LD_PRELOAD", "", true)
+Gio.Resource.load("@PKGDATADIR@/data.gresource")._register()
+
+const module = await import("resource:///index.js")
+exit(await module.default.main(programArgs))
diff --git a/examples/gtk4/simple-bar/js/src/main.in.sh b/examples/gtk4/simple-bar/js/src/main.in.sh
new file mode 100755
index 0000000..32a0326
--- /dev/null
+++ b/examples/gtk4/simple-bar/js/src/main.in.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+LD_PRELOAD="@LAYER_SHELL_PREFIX@/lib/libgtk4-layer-shell.so" "@INDEX@"
diff --git a/examples/gtk4/simple-bar/js/src/main.scss b/examples/gtk4/simple-bar/js/src/main.scss
new file mode 100644
index 0000000..c37695a
--- /dev/null
+++ b/examples/gtk4/simple-bar/js/src/main.scss
@@ -0,0 +1 @@
+@use "./scss/Bar.scss";
diff --git a/examples/gtk4/simple-bar/js/src/meson.build b/examples/gtk4/simple-bar/js/src/meson.build
new file mode 100644
index 0000000..8d03182
--- /dev/null
+++ b/examples/gtk4/simple-bar/js/src/meson.build
@@ -0,0 +1,84 @@
+pkgdatadir = get_option('prefix') / get_option('datadir') / meson.project_name()
+bindir = get_option('prefix') / get_option('bindir')
+blp = find_program('blueprint-compiler', required: true)
+sass = find_program('sass', required: true)
+esbuild = find_program('esbuild', required: true)
+gjs = find_program('gjs', required: true)
+layer_shell = dependency('gtk4-layer-shell-0')
+
+blueprint_sources = files(
+ 'ui/Bar.blp',
+)
+
+# transplie blueprints
+ui = custom_target(
+ 'blueprint',
+ input: blueprint_sources,
+ output: '.',
+ command: [
+ blp,
+ 'batch-compile',
+ '@OUTPUT@',
+ '@CURRENT_SOURCE_DIR@',
+ '@INPUT@',
+ ],
+)
+
+# bundle ts files
+js = custom_target(
+ 'typescript',
+ input: files('ts/App.ts'),
+ command: [
+ esbuild,
+ '--bundle', '@INPUT@',
+ '--format=esm',
+ '--outfile=@OUTPUT@',
+ '--sourcemap=inline',
+ '--external:gi://*',
+ '--external:gettext',
+ '--external:system',
+ ],
+ output: 'index.js',
+)
+
+# bundle scss files
+css = custom_target(
+ 'scss',
+ input: files('main.scss'),
+ command: [sass, '@INPUT@', '@OUTPUT@'],
+ output: 'main.css',
+)
+
+# compiling source files into a binary
+import('gnome').compile_resources(
+ 'data',
+ files('gresource.xml'),
+ dependencies: [ui, css, js],
+ gresource_bundle: true,
+ install: true,
+ install_dir: pkgdatadir,
+)
+
+# gresource can't be run with gjs, we still need an entry script
+configure_file(
+ input: files('main.in.js'),
+ output: 'main.js',
+ configuration: {
+ 'GJS': gjs.full_path(),
+ 'PKGDATADIR': pkgdatadir,
+ },
+ install: true,
+ install_dir: pkgdatadir,
+)
+
+# and we need to wrap the entry script to preload gtk4-layer-shell
+configure_file(
+ input: 'main.in.sh',
+ output: meson.project_name(),
+ configuration: {
+ 'LAYER_SHELL_PREFIX': layer_shell.get_variable('prefix'),
+ 'INDEX': pkgdatadir / 'main.js',
+ },
+ install: true,
+ install_dir: bindir,
+)
diff --git a/examples/gtk4/simple-bar/js/src/scss/Bar.scss b/examples/gtk4/simple-bar/js/src/scss/Bar.scss
new file mode 100644
index 0000000..86ea856
--- /dev/null
+++ b/examples/gtk4/simple-bar/js/src/scss/Bar.scss
@@ -0,0 +1,19 @@
+// https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-4-16/gtk/theme/Default/_colors-public.scss
+$fg-color: #{"@theme_fg_color"};
+$bg-color: #{"@theme_bg_color"};
+
+window.Bar {
+ > box {
+ background: $bg-color;
+ color: $fg-color;
+ font-weight: bold;
+ }
+
+ button {
+ min-height: 0;
+ min-width: 0;
+ border-radius: 8px;
+ margin: 4px;
+ padding: 4px 8px;
+ }
+}
diff --git a/examples/gtk4/simple-bar/js/src/ts/App.ts b/examples/gtk4/simple-bar/js/src/ts/App.ts
new file mode 100644
index 0000000..012fafa
--- /dev/null
+++ b/examples/gtk4/simple-bar/js/src/ts/App.ts
@@ -0,0 +1,52 @@
+import GObject from "gi://GObject"
+import Astal from "gi://Astal?version=4.0"
+import Gio from "gi://Gio"
+import GLib from "gi://GLib"
+import AstalIO from "gi://AstalIO"
+import Bar from "./Bar"
+
+export default class App extends Astal.Application {
+ static {
+ GObject.registerClass(this)
+ }
+
+ static instance: App
+ static instanceName = "simple-bar"
+
+ // this is where request handlers can be implemented
+ // that will be used to handle `astal` cli invocations
+ vfunc_request(request: string, conn: Gio.SocketConnection): void {
+ print("incoming request", request)
+ AstalIO.write_sock(conn, "response", null)
+ }
+
+ // this is the method that will be invoked on `app.runAsync()`
+ // this is where everything should be initialized and instantiated
+ vfunc_activate(): void {
+ this.apply_css("resource:///main.css", false)
+ this.add_window(new Bar())
+ }
+
+ // entry point of our app
+ static async main(argv: string[]): Promise<number> {
+ GLib.set_prgname(App.instanceName)
+ App.instance = new App({ instanceName: App.instanceName })
+
+ try {
+ // `app.acquire_socket()` needed for the request API to work
+ App.instance.acquire_socket()
+
+ // if it succeeds we can run the app
+ return await App.instance.runAsync([])
+ } catch (error) {
+ // if it throws an error it means there is already an instance
+ // with `instanceName` running, so we just send a request instead
+ const response = AstalIO.send_request(
+ App.instanceName,
+ argv.join(" "),
+ )
+ print(response)
+ return 0
+ }
+ }
+}
diff --git a/examples/gtk4/simple-bar/js/src/ts/Bar.ts b/examples/gtk4/simple-bar/js/src/ts/Bar.ts
new file mode 100644
index 0000000..a81623c
--- /dev/null
+++ b/examples/gtk4/simple-bar/js/src/ts/Bar.ts
@@ -0,0 +1,189 @@
+import Astal from "gi://Astal?version=4.0"
+import AstalIO from "gi://AstalIO"
+import GLib from "gi://GLib"
+import Gtk from "gi://Gtk?version=4.0"
+import GObject from "gi://GObject?version=2.0"
+import AstalBattery from "gi://AstalBattery"
+import AstalWp from "gi://AstalWp"
+import AstalNetwork from "gi://AstalNetwork"
+import AstalMpris from "gi://AstalMpris"
+import AstalPowerProfiles from "gi://AstalPowerProfiles"
+import AstalTray from "gi://AstalTray"
+import AstalBluetooth from "gi://AstalBluetooth"
+import { string, number, boolean } from "./props"
+
+const { TOP, LEFT, RIGHT } = Astal.WindowAnchor
+const SYNC = GObject.BindingFlags.SYNC_CREATE
+
+export default class Bar extends Astal.Window {
+ static {
+ GObject.registerClass(
+ {
+ GTypeName: "Bar",
+ Template: "resource:///ui/Bar.ui",
+ InternalChildren: ["popover", "calendar", "traybox"],
+ Properties: {
+ ...string("clock"),
+ ...string("volume-icon"),
+ ...boolean("battery-visible"),
+ ...string("battery-label"),
+ ...string("battery-icon"),
+ ...number("volume", 0, 1),
+ ...string("network-icon"),
+ ...boolean("mpris-visible"),
+ ...string("mpris-label"),
+ ...string("mpris-art"),
+ ...string("power-profile-icon"),
+ ...boolean("bluetooth-visible"),
+ },
+ },
+ this,
+ )
+ }
+
+ declare clock: string
+ declare battery_label: string
+ declare mpris_label: string
+ declare bluetooth_visible: string
+
+ declare _popover: Gtk.Popover
+ declare _calendar: Gtk.Calendar
+ declare _traybox: Gtk.Box
+
+ constructor() {
+ super({
+ visible: true,
+ exclusivity: Astal.Exclusivity.EXCLUSIVE,
+ anchor: TOP | LEFT | RIGHT,
+ cssClasses: ["Bar"],
+ })
+
+ // clock
+ const timer = AstalIO.Time.interval(1000, () => {
+ this.clock = GLib.DateTime.new_now_local().format("%H:%M:%S")!
+ })
+ this.connect("destroy", () => timer.cancel())
+
+ // everytime popover is opened, select current day
+ this._popover.connect("notify::visible", ({ visible }) => {
+ if (visible) {
+ this._calendar.select_day(GLib.DateTime.new_now_local())
+ }
+ })
+
+ // network
+ const nw = AstalNetwork.get_default()
+ let networkBinding: GObject.Binding
+
+ // @ts-expect-error mistyped
+ nw.bind_property_full(
+ "primary",
+ this,
+ "network-icon",
+ SYNC,
+ (_, primary: AstalNetwork.Primary) => {
+ networkBinding?.unbind()
+
+ switch (primary) {
+ case AstalNetwork.Primary.WIRED:
+ networkBinding = nw.wired.bind_property(
+ "icon-name",
+ this,
+ "network-icon",
+ SYNC,
+ )
+ return [false, ""]
+ case AstalNetwork.Primary.WIFI:
+ networkBinding = nw.wifi.bind_property(
+ "icon-name",
+ this,
+ "network-icon",
+ SYNC,
+ )
+ return [false, ""]
+ default:
+ return [true, "network-idle-symbolic"]
+ }
+ },
+ null,
+ )
+
+ // battery
+ const bat = AstalBattery.get_default()
+
+ bat.bind_property("is-present", this, "battery-visible", SYNC)
+ bat.bind_property("icon-name", this, "battery-icon", SYNC)
+
+ this.battery_label = `${Math.floor(bat.percentage * 100)}%`
+ const batteryId = bat.connect("notify::percentage", () => {
+ this.battery_label = `${Math.floor(bat.percentage * 100)}%`
+ })
+ this.connect("destroy", () => bat.disconnect(batteryId))
+
+ // volume
+ const speaker = AstalWp.get_default()!.defaultSpeaker
+ speaker.bind_property("volume-icon", this, "volume-icon", SYNC)
+ speaker.bind_property("volume", this, "volume", SYNC)
+
+ // mpris
+ const player = AstalMpris.Player.new("spotify")
+ player.bind_property("available", this, "mpris-visible", SYNC)
+ player.bind_property("cover-art", this, "mpris-art", SYNC)
+
+ this.mpris_label = `${player.artist} - ${player.title}`
+ const playerId = player.connect("notify::metadata", () => {
+ this.mpris_label = `${player.artist} - ${player.title}`
+ })
+ this.connect("destroy", () => player.disconnect(playerId))
+
+ // powerprofiles
+ const powerprofile = AstalPowerProfiles.get_default()
+ powerprofile.bind_property(
+ "icon-name",
+ this,
+ "power-profile-icon",
+ SYNC,
+ )
+
+ // tray
+ const tray = AstalTray.get_default()
+ const trayItems = new Map<string, Gtk.MenuButton>()
+ const trayId1 = tray.connect("item-added", (_, id) => {
+ const item = tray.get_item(id)
+ const popover = Gtk.PopoverMenu.new_from_model(item.menu_model)
+ const icon = new Gtk.Image()
+ const button = new Gtk.MenuButton({ popover, child: icon })
+
+ item.bind_property("gicon", icon, "gicon", SYNC)
+ popover.insert_action_group("dbusmenu", item.action_group)
+ item.connect("notify::action-group", () => {
+ popover.insert_action_group("dbusmenu", item.action_group)
+ })
+
+ trayItems.set(id, button)
+ this._traybox.append(button)
+ })
+
+ const trayId2 = tray.connect("item-removed", (_, id) => {
+ const button = trayItems.get(id)
+ if (button) {
+ this._traybox.remove(button)
+ button.run_dispose()
+ trayItems.delete(id)
+ }
+ })
+
+ this.connect("destroy", () => {
+ tray.disconnect(trayId1)
+ tray.disconnect(trayId2)
+ })
+
+ // bluetooth
+ const bt = AstalBluetooth.get_default()
+ bt.bind_property("is-connected", this, "bluetooth-visible", SYNC)
+ }
+
+ change_volume(_scale: Gtk.Scale, _type: Gtk.ScrollType, value: number) {
+ AstalWp.get_default()?.defaultSpeaker.set_volume(value)
+ }
+}
diff --git a/examples/gtk4/simple-bar/js/src/ts/props.ts b/examples/gtk4/simple-bar/js/src/ts/props.ts
new file mode 100644
index 0000000..dfa79ee
--- /dev/null
+++ b/examples/gtk4/simple-bar/js/src/ts/props.ts
@@ -0,0 +1,45 @@
+import GObject from "gi://GObject?version=2.0"
+
+export function string(name: string, defaultValue = "") {
+ return {
+ [name]: GObject.ParamSpec.string(
+ name,
+ null,
+ null,
+ GObject.ParamFlags.READWRITE,
+ defaultValue,
+ ),
+ }
+}
+
+export function number(
+ name: string,
+ min = -Number.MIN_SAFE_INTEGER,
+ max = Number.MAX_SAFE_INTEGER,
+ defaultValue = 0,
+) {
+ return {
+ [name]: GObject.ParamSpec.double(
+ name,
+ null,
+ null,
+ GObject.ParamFlags.READWRITE,
+ min,
+ max,
+ defaultValue,
+ ),
+ }
+}
+
+export function boolean(name: string, defaultValue = false) {
+ return {
+ [name]: GObject.ParamSpec.boolean(
+ name,
+ null,
+ null,
+ GObject.ParamFlags.READWRITE,
+ defaultValue,
+ ),
+ }
+}
+
diff --git a/examples/gtk4/simple-bar/js/src/ui/Bar.blp b/examples/gtk4/simple-bar/js/src/ui/Bar.blp
new file mode 100644
index 0000000..6e401e7
--- /dev/null
+++ b/examples/gtk4/simple-bar/js/src/ui/Bar.blp
@@ -0,0 +1,84 @@
+using Gtk 4.0;
+using Astal 4.0;
+
+template $Bar: Astal.Window {
+ CenterBox centerbox {
+ start-widget: Box {
+ MenuButton {
+ Label {
+ label: bind template.clock;
+ }
+
+ popover: Popover popover {
+ Calendar calendar {
+ show-day-names: true;
+ show-heading: true;
+ show-week-numbers: true;
+ }
+ };
+ }
+ };
+
+ center-widget: Box {
+ Box {
+ visible: bind template.mpris-visible;
+
+ Image {
+ file: bind template.mpris-art;
+ }
+
+ Label {
+ label: bind template.mpris-label;
+ }
+ }
+ };
+
+ end-widget: Box {
+ spacing: 4;
+
+ Image {
+ visible: bind template.bluetooth-visible;
+ icon-name: "bluetooth-symbolic";
+ }
+
+ Image {
+ icon-name: bind template.power-profile-icon;
+ }
+
+ Image {
+ icon-name: bind template.network-icon;
+ }
+
+ Box {
+ Image {
+ icon-name: bind template.volume-icon;
+ }
+
+ Scale {
+ width-request: 100;
+ change-value => $change_volume();
+
+ adjustment: Adjustment {
+ value: bind template.volume;
+ lower: 0;
+ upper: 1;
+ };
+ }
+ }
+
+ Box {
+ Image {
+ icon-name: bind template.battery-icon;
+ }
+
+ Label {
+ label: bind template.battery-label;
+ }
+ }
+
+ Box traybox {
+ spacing: 4;
+ }
+ };
+ }
+}
diff --git a/examples/gtk4/simple-bar/js/tsconfig.json b/examples/gtk4/simple-bar/js/tsconfig.json
new file mode 100644
index 0000000..65b3aba
--- /dev/null
+++ b/examples/gtk4/simple-bar/js/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "compilerOptions": {
+ "strict": true,
+ "target": "ES2022",
+ "module": "ES2022",
+ "moduleResolution": "Bundler"
+ }
+}
diff --git a/examples/gtk4/simple-bar/py/.gitignore b/examples/gtk4/simple-bar/py/.gitignore
new file mode 100644
index 0000000..bb252c6
--- /dev/null
+++ b/examples/gtk4/simple-bar/py/.gitignore
@@ -0,0 +1,3 @@
+__pycache__/
+build/
+result
diff --git a/examples/gtk4/simple-bar/py/README.md b/examples/gtk4/simple-bar/py/README.md
new file mode 100644
index 0000000..25f296b
--- /dev/null
+++ b/examples/gtk4/simple-bar/py/README.md
@@ -0,0 +1,45 @@
+# Simple Astal Bar example in Python
+
+This example shows you how to get a Python+Blueprint+Sass project going.
+
+## Dependencies
+
+- python3
+- pygobject
+- meson
+- blueprint-compiler
+- sass
+- astal4
+- astal-battery
+- astal-wireplumber
+- astak-network
+- astal-mpris
+- astak-powerprofiles
+- astal-tray
+- astal-bluetooth
+
+## How to use
+
+> [!NOTE]
+> If you are on Nix, there is an example flake included
+> otherwise feel free to `rm flake.nix`
+
+- developing
+
+ ```sh
+ meson setup build --wipe --prefix "$pwd/result"
+ meson install -C build
+ ./result/bin/simple-bar
+ ```
+
+- installing
+
+ ```sh
+ meson setup build --wipe --prefix /usr
+ meson install -C build
+ simple-bar
+ ```
+
+- adding new python files requires no additional steps
+- adding new scss files requires no additional steps as long as they are imported from `main.scss`
+- adding new ui (blueprint) files will also have to be listed in `meson.build` and in `gresource.xml`
diff --git a/examples/gtk4/simple-bar/py/flake.nix b/examples/gtk4/simple-bar/py/flake.nix
new file mode 100644
index 0000000..4002826
--- /dev/null
+++ b/examples/gtk4/simple-bar/py/flake.nix
@@ -0,0 +1,55 @@
+{
+ inputs = {
+ nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
+ astal = {
+ url = "github:aylur/astal";
+ inputs.nixpkgs.follows = "nixpkgs";
+ };
+ };
+
+ outputs = {
+ self,
+ nixpkgs,
+ astal,
+ }: let
+ system = "x86_64-linux";
+ pkgs = nixpkgs.legacyPackages.${system};
+
+ python = pkgs.python3.withPackages (ps: [
+ ps.pygobject3
+ ]);
+
+ nativeBuildInputs = with pkgs; [
+ meson
+ ninja
+ pkg-config
+ gobject-introspection
+ wrapGAppsHook4
+ blueprint-compiler
+ dart-sass
+ ];
+
+ astalPackages = with astal.packages.${system}; [
+ io
+ astal4
+ battery
+ wireplumber
+ network
+ mpris
+ powerprofiles
+ tray
+ bluetooth
+ ];
+ in {
+ packages.${system}.default = pkgs.stdenv.mkDerivation {
+ name = "simple-bar";
+ src = ./.;
+ inherit nativeBuildInputs;
+ buildInputs = astalPackages ++ [python];
+ };
+
+ devShells.${system}.default = pkgs.mkShell {
+ packages = nativeBuildInputs ++ astalPackages ++ [python];
+ };
+ };
+}
diff --git a/examples/gtk4/simple-bar/py/meson.build b/examples/gtk4/simple-bar/py/meson.build
new file mode 100644
index 0000000..f49af2e
--- /dev/null
+++ b/examples/gtk4/simple-bar/py/meson.build
@@ -0,0 +1,12 @@
+project('simple-bar')
+
+dependency('astal-4-4.0')
+dependency('astal-battery-0.1')
+dependency('astal-wireplumber-0.1')
+dependency('astal-network-0.1')
+dependency('astal-mpris-0.1')
+dependency('astal-power-profiles-0.1')
+dependency('astal-tray-0.1')
+dependency('astal-bluetooth-0.1')
+
+subdir('src')
diff --git a/examples/gtk4/simple-bar/py/src/gresource.xml b/examples/gtk4/simple-bar/py/src/gresource.xml
new file mode 100644
index 0000000..36dad8c
--- /dev/null
+++ b/examples/gtk4/simple-bar/py/src/gresource.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/">
+ <file>ui/Bar.ui</file>
+ <file>main.css</file>
+ </gresource>
+</gresources>
diff --git a/examples/gtk4/simple-bar/py/src/main.in.py b/examples/gtk4/simple-bar/py/src/main.in.py
new file mode 100755
index 0000000..58042f4
--- /dev/null
+++ b/examples/gtk4/simple-bar/py/src/main.in.py
@@ -0,0 +1,19 @@
+#!@PYTHON@
+
+import gi
+
+gi.require_version("Gio", "2.0")
+
+from gi.repository import Gio
+from sys import argv, path
+from ctypes import CDLL
+
+CDLL("@LAYER_SHELL_PREFIX@/lib/libgtk4-layer-shell.so")
+path.insert(1, "@PKGDATADIR@")
+Gio.Resource.load("@PKGDATADIR@/data.gresource")._register()
+
+
+if __name__ == "__main__":
+ from py.App import App
+
+ App.main(argv[2:])
diff --git a/examples/gtk4/simple-bar/py/src/main.scss b/examples/gtk4/simple-bar/py/src/main.scss
new file mode 100644
index 0000000..c37695a
--- /dev/null
+++ b/examples/gtk4/simple-bar/py/src/main.scss
@@ -0,0 +1 @@
+@use "./scss/Bar.scss";
diff --git a/examples/gtk4/simple-bar/py/src/meson.build b/examples/gtk4/simple-bar/py/src/meson.build
new file mode 100644
index 0000000..54ecd17
--- /dev/null
+++ b/examples/gtk4/simple-bar/py/src/meson.build
@@ -0,0 +1,59 @@
+pkgdatadir = get_option('prefix') / get_option('datadir') / meson.project_name()
+bindir = get_option('prefix') / get_option('bindir')
+
+blp = find_program('blueprint-compiler', required: true)
+sass = find_program('sass', required: true)
+python = find_program('python3', required: true)
+layer_shell = dependency('gtk4-layer-shell-0')
+
+blueprint_sources = files(
+ 'ui/Bar.blp',
+)
+
+# transplie blueprints
+ui = custom_target(
+ 'blueprint',
+ input: blueprint_sources,
+ output: '.',
+ command: [
+ blp,
+ 'batch-compile',
+ '@OUTPUT@',
+ '@CURRENT_SOURCE_DIR@',
+ '@INPUT@',
+ ],
+)
+
+# bundle styles
+css = custom_target(
+ 'scss',
+ input: files('main.scss'),
+ command: [sass, '@INPUT@', '@OUTPUT@'],
+ output: 'main.css',
+)
+
+# compiling ui and css into a binary
+import('gnome').compile_resources(
+ 'data',
+ files('gresource.xml'),
+ dependencies: [ui, css],
+ gresource_bundle: true,
+ install: true,
+ install_dir: pkgdatadir,
+)
+
+# install python sources
+install_subdir('py', install_dir: pkgdatadir)
+
+# configure the main python entry file
+configure_file(
+ input: 'main.in.py',
+ output: meson.project_name(),
+ configuration: {
+ 'PYTHON': python.full_path(),
+ 'LAYER_SHELL_PREFIX': layer_shell.get_variable('prefix'),
+ 'PKGDATADIR': pkgdatadir,
+ },
+ install: true,
+ install_dir: bindir,
+)
diff --git a/examples/gtk4/simple-bar/py/src/py/App.py b/examples/gtk4/simple-bar/py/src/py/App.py
new file mode 100644
index 0000000..678fb5f
--- /dev/null
+++ b/examples/gtk4/simple-bar/py/src/py/App.py
@@ -0,0 +1,41 @@
+from pathlib import Path
+from gi.repository import Gio, GLib, Astal, GLib, AstalIO
+from py.Bar import Bar
+
+
+class App(Astal.Application):
+ __gtype_name__ = "App"
+ instance_name = "simple-bar"
+
+ # this is where request handlers can be implemented
+ # that will be used to handle `astal` cli invocations
+ def do_astal_application_request(self, request, conn):
+ print("incoming request", request)
+ AstalIO.write_sock(conn, "response", None)
+
+ # this is the method that will be invoked on `app.run()`
+ # this is where everything should be initialized and instantiated
+ def do_activate(self):
+ self.apply_css("resource:///main.css", False)
+ self.add_window(Bar())
+
+ @staticmethod
+ def main(argv):
+ GLib.set_prgname(App.instance_name)
+ App.instance = App(instance_name=App.instance_name)
+
+ try:
+ # `app.acquire_socket()` needed for the request API to work
+ App.instance.acquire_socket()
+
+ # if it succeeds we can run the app
+ return App.instance.run([])
+ except Exception:
+ # if it throws an error it means there is already an instance
+ # with `instance_name` running, so we just send a request instead
+ response = AstalIO.send_request(
+ App.instance_name,
+ argv.join(" "),
+ )
+ print(response)
+ return 0
diff --git a/examples/gtk4/simple-bar/py/src/py/Bar.py b/examples/gtk4/simple-bar/py/src/py/Bar.py
new file mode 100644
index 0000000..cf521a9
--- /dev/null
+++ b/examples/gtk4/simple-bar/py/src/py/Bar.py
@@ -0,0 +1,184 @@
+import math
+from gi.repository import (
+ Astal,
+ AstalIO,
+ GObject,
+ GLib,
+ Gtk,
+ GObject,
+ AstalBattery,
+ AstalWp,
+ AstalNetwork,
+ AstalMpris,
+ AstalPowerProfiles,
+ AstalTray,
+ AstalBluetooth,
+)
+
+SYNC = GObject.BindingFlags.SYNC_CREATE
+
+
[email protected](resource_path="/ui/Bar.ui")
+class Bar(Astal.Window):
+ __gtype_name__ = "Bar"
+
+ clock = GObject.Property(type=str)
+ volume_icon = GObject.Property(type=str)
+ battery_visible = GObject.Property(type=bool, default=False)
+ battery_label = GObject.Property(type=str)
+ battery_icon = GObject.Property(type=str)
+ volume = GObject.Property(type=float)
+ network_icon = GObject.Property(type=str)
+ mpris_visible = GObject.Property(type=bool, default=False)
+ mpris_label = GObject.Property(type=str)
+ mpris_art = GObject.Property(type=str)
+ power_profile_icon = GObject.Property(type=str)
+ bluetooth_visible = GObject.Property(type=bool, default=False)
+
+ popover = Gtk.Template.Child()
+ calendar = Gtk.Template.Child()
+ traybox = Gtk.Template.Child()
+
+ def __init__(self):
+ super().__init__(
+ anchor=Astal.WindowAnchor.TOP
+ | Astal.WindowAnchor.LEFT
+ | Astal.WindowAnchor.RIGHT,
+ exclusivity=Astal.Exclusivity.EXCLUSIVE,
+ css_classes=["Bar"],
+ visible=True,
+ )
+
+ # clock
+ timer = AstalIO.Time.interval(1000, self.set_clock)
+ self.connect("destroy", lambda _: timer.cancel())
+
+ # everytime popover is opened, select current day
+ self.popover.connect("notify::visible", self.on_popover_visible)
+
+ # network
+ nw = AstalNetwork.get_default()
+ self._network_binding = None
+
+ nw.bind_property(
+ "primary",
+ self,
+ "network-icon",
+ SYNC,
+ self.on_nm_primary,
+ None,
+ )
+
+ # battery
+ bat = AstalBattery.get_default()
+ bat.bind_property("is-present", self, "battery-visible", SYNC)
+ bat.bind_property("icon-name", self, "battery-icon", SYNC)
+ bat.bind_property(
+ "percentage",
+ self,
+ "battery-label",
+ SYNC,
+ lambda _, percentage: f"{math.floor(percentage * 100)}%",
+ None,
+ )
+
+ # volume
+ speaker = AstalWp.get_default().get_default_speaker()
+ speaker.bind_property("volume-icon", self, "volume-icon", SYNC)
+ speaker.bind_property("volume", self, "volume", SYNC)
+
+ # mpris
+ player = AstalMpris.Player.new("spotify")
+ player.bind_property("available", self, "mpris-visible", SYNC)
+ player.bind_property("cover-art", self, "mpris-art", SYNC)
+ player.bind_property(
+ "metadata",
+ self,
+ "mpris-label",
+ SYNC,
+ lambda *_: f"{player.get_artist()} - {player.get_title()}",
+ None,
+ )
+
+ # powerprofiles
+ powerprofile = AstalPowerProfiles.get_default()
+ powerprofile.bind_property("icon-name", self, "power-profile-icon", SYNC)
+
+ # bluetooth
+ bt = AstalBluetooth.get_default()
+ bt.bind_property("is-connected", self, "bluetooth-visible", SYNC)
+
+ # tray
+ tray = AstalTray.get_default()
+ self._tray_items = {}
+
+ def on_tray_item_added(tray, id):
+ item = tray.get_item(id)
+ popover = Gtk.PopoverMenu.new_from_model(item.get_menu_model())
+ icon = Gtk.Image()
+ button = Gtk.MenuButton(popover=popover, child=icon)
+
+ item.bind_property("gicon", icon, "gicon", SYNC)
+ popover.insert_action_group("dbusmenu", item.get_action_group())
+ item.connect(
+ "notify::action-group",
+ lambda *_: popover.insert_action_group(
+ "dbusmenu", item.get_action_group()
+ ),
+ )
+
+ self._tray_items[id] = button
+ self.traybox.append(button)
+
+ def on_tray_item_removed(_tray, id):
+ button = self._tray_items.get(id)
+ if button:
+ self.traybox.remove(button)
+ button.run_dispose()
+ del self._tray_items[id]
+
+ tray.connect("item_added", on_tray_item_added)
+ tray.connect("item_removed", on_tray_item_removed)
+ self.connect(
+ "destroy",
+ lambda *_: (
+ tray.disconnect(on_tray_item_added),
+ tray.disconnect(on_tray_item_removed),
+ ),
+ )
+
+ def on_popover_visible(self, popover, _pspec):
+ if popover.get_visible():
+ self.calendar.select_day(GLib.DateTime.new_now_local())
+
+ def on_nm_primary(self, _binding, primary):
+ nw = AstalNetwork.get_default()
+ if self._network_binding is not None:
+ self._network_binding.unbind()
+
+ match primary:
+ case AstalNetwork.Primary.WIRED:
+ self._network_binding = nw.get_wired().bind_property(
+ "icon-name",
+ self,
+ "network-icon",
+ SYNC,
+ )
+ return nw.get_wired().get_icon_name()
+ case AstalNetwork.Primary.WIFI:
+ self._network_binding = nw.get_wifi().bind_property(
+ "icon-name",
+ self,
+ "network-icon",
+ SYNC,
+ )
+ return nw.get_wifi().get_icon_name()
+ case _:
+ return "network-idle-symbolic"
+
+ def set_clock(self):
+ self.clock = GLib.DateTime.new_now_local().format("%H:%M:%S")
+
+ @Gtk.Template.Callback()
+ def change_volume(self, _scale, _type, value):
+ AstalWp.get_default().get_default_speaker().set_volume(value)
diff --git a/examples/gtk4/simple-bar/py/src/py/__init__.py b/examples/gtk4/simple-bar/py/src/py/__init__.py
new file mode 100644
index 0000000..57f4b1d
--- /dev/null
+++ b/examples/gtk4/simple-bar/py/src/py/__init__.py
@@ -0,0 +1,15 @@
+import gi
+
+gi.require_version("GObject", "2.0")
+gi.require_version("GLib", "2.0")
+gi.require_version("Gtk", "4.0")
+gi.require_version("AstalIO", "0.1")
+gi.require_version("Astal", "4.0")
+
+gi.require_version("AstalBattery", "0.1")
+gi.require_version("AstalWp", "0.1")
+gi.require_version("AstalNetwork", "0.1")
+gi.require_version("AstalMpris", "0.1")
+gi.require_version("AstalPowerProfiles", "0.1")
+gi.require_version("AstalTray", "0.1")
+gi.require_version("AstalBluetooth", "0.1")
diff --git a/examples/gtk4/simple-bar/py/src/scss/Bar.scss b/examples/gtk4/simple-bar/py/src/scss/Bar.scss
new file mode 100644
index 0000000..86ea856
--- /dev/null
+++ b/examples/gtk4/simple-bar/py/src/scss/Bar.scss
@@ -0,0 +1,19 @@
+// https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-4-16/gtk/theme/Default/_colors-public.scss
+$fg-color: #{"@theme_fg_color"};
+$bg-color: #{"@theme_bg_color"};
+
+window.Bar {
+ > box {
+ background: $bg-color;
+ color: $fg-color;
+ font-weight: bold;
+ }
+
+ button {
+ min-height: 0;
+ min-width: 0;
+ border-radius: 8px;
+ margin: 4px;
+ padding: 4px 8px;
+ }
+}
diff --git a/examples/gtk4/simple-bar/py/src/ui/Bar.blp b/examples/gtk4/simple-bar/py/src/ui/Bar.blp
new file mode 100644
index 0000000..6e401e7
--- /dev/null
+++ b/examples/gtk4/simple-bar/py/src/ui/Bar.blp
@@ -0,0 +1,84 @@
+using Gtk 4.0;
+using Astal 4.0;
+
+template $Bar: Astal.Window {
+ CenterBox centerbox {
+ start-widget: Box {
+ MenuButton {
+ Label {
+ label: bind template.clock;
+ }
+
+ popover: Popover popover {
+ Calendar calendar {
+ show-day-names: true;
+ show-heading: true;
+ show-week-numbers: true;
+ }
+ };
+ }
+ };
+
+ center-widget: Box {
+ Box {
+ visible: bind template.mpris-visible;
+
+ Image {
+ file: bind template.mpris-art;
+ }
+
+ Label {
+ label: bind template.mpris-label;
+ }
+ }
+ };
+
+ end-widget: Box {
+ spacing: 4;
+
+ Image {
+ visible: bind template.bluetooth-visible;
+ icon-name: "bluetooth-symbolic";
+ }
+
+ Image {
+ icon-name: bind template.power-profile-icon;
+ }
+
+ Image {
+ icon-name: bind template.network-icon;
+ }
+
+ Box {
+ Image {
+ icon-name: bind template.volume-icon;
+ }
+
+ Scale {
+ width-request: 100;
+ change-value => $change_volume();
+
+ adjustment: Adjustment {
+ value: bind template.volume;
+ lower: 0;
+ upper: 1;
+ };
+ }
+ }
+
+ Box {
+ Image {
+ icon-name: bind template.battery-icon;
+ }
+
+ Label {
+ label: bind template.battery-label;
+ }
+ }
+
+ Box traybox {
+ spacing: 4;
+ }
+ };
+ }
+}
diff --git a/examples/gtk4/simple-bar/vala/.gitignore b/examples/gtk4/simple-bar/vala/.gitignore
new file mode 100644
index 0000000..567609b
--- /dev/null
+++ b/examples/gtk4/simple-bar/vala/.gitignore
@@ -0,0 +1 @@
+build/
diff --git a/examples/gtk4/simple-bar/vala/README.md b/examples/gtk4/simple-bar/vala/README.md
new file mode 100644
index 0000000..a1a3eae
--- /dev/null
+++ b/examples/gtk4/simple-bar/vala/README.md
@@ -0,0 +1,44 @@
+# Simple Astal Bar example in Vala
+
+This example shows you how to get a Vala+Blueprint+Sass project going.
+
+## Dependencies
+
+- vala
+- meson
+- blueprint-compiler
+- sass
+- astal4
+- astal-battery
+- astal-wireplumber
+- astak-network
+- astal-mpris
+- astak-power-profiles
+- astal-tray
+- astal-bluetooth
+
+## How to use
+
+> [!NOTE]
+> If you are on Nix, there is an example flake included
+> otherwise feel free to `rm flake.nix`
+
+- developing
+
+ ```sh
+ meson setup build --wipe --prefix "$pwd/result"
+ meson install -C build
+ ./result/bin/simple-bar
+ ```
+
+- installing
+
+ ```sh
+ meson setup build --wipe --prefix /usr
+ meson install -C build
+ simple-bar
+ ```
+
+- adding new vala files will also have to be listed in `meson.build`
+- adding new scss files requires no additional steps as long as they are imported from `main.scss`
+- adding new ui (blueprint) files will also have to be listed in `meson.build` and in `gresource.xml`
diff --git a/examples/gtk4/simple-bar/vala/flake.nix b/examples/gtk4/simple-bar/vala/flake.nix
new file mode 100644
index 0000000..52db45b
--- /dev/null
+++ b/examples/gtk4/simple-bar/vala/flake.nix
@@ -0,0 +1,52 @@
+{
+ inputs = {
+ nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
+ astal = {
+ url = "github:aylur/astal";
+ inputs.nixpkgs.follows = "nixpkgs";
+ };
+ };
+
+ outputs = {
+ self,
+ nixpkgs,
+ astal,
+ }: let
+ system = "x86_64-linux";
+ pkgs = nixpkgs.legacyPackages.${system};
+
+ nativeBuildInputs = with pkgs; [
+ meson
+ ninja
+ pkg-config
+ gobject-introspection
+ wrapGAppsHook4
+ blueprint-compiler
+ dart-sass
+ vala
+ ];
+
+ astalPackages = with astal.packages.${system}; [
+ io
+ astal4
+ battery
+ wireplumber
+ network
+ mpris
+ powerprofiles
+ tray
+ bluetooth
+ ];
+ in {
+ packages.${system}.default = pkgs.stdenv.mkDerivation {
+ name = "simple-bar";
+ src = ./.;
+ inherit nativeBuildInputs;
+ buildInputs = astalPackages;
+ };
+
+ devShells.${system}.default = pkgs.mkShell {
+ packages = nativeBuildInputs ++ astalPackages;
+ };
+ };
+}
diff --git a/examples/gtk4/simple-bar/vala/meson.build b/examples/gtk4/simple-bar/vala/meson.build
new file mode 100644
index 0000000..6858ee0
--- /dev/null
+++ b/examples/gtk4/simple-bar/vala/meson.build
@@ -0,0 +1,3 @@
+project('simple-bar', 'vala')
+
+subdir('src')
diff --git a/examples/gtk4/simple-bar/vala/src/gresource.xml b/examples/gtk4/simple-bar/vala/src/gresource.xml
new file mode 100644
index 0000000..abc50e5
--- /dev/null
+++ b/examples/gtk4/simple-bar/vala/src/gresource.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/">
+ <file>main.css</file>
+ <file>ui/Bar.ui</file>
+ </gresource>
+</gresources>
diff --git a/examples/gtk4/simple-bar/vala/src/main.scss b/examples/gtk4/simple-bar/vala/src/main.scss
new file mode 100644
index 0000000..c37695a
--- /dev/null
+++ b/examples/gtk4/simple-bar/vala/src/main.scss
@@ -0,0 +1 @@
+@use "./scss/Bar.scss";
diff --git a/examples/gtk4/simple-bar/vala/src/meson.build b/examples/gtk4/simple-bar/vala/src/meson.build
new file mode 100644
index 0000000..a11da46
--- /dev/null
+++ b/examples/gtk4/simple-bar/vala/src/meson.build
@@ -0,0 +1,67 @@
+pkgdatadir = get_option('prefix') / get_option('datadir')
+bindir = get_option('prefix') / get_option('bindir')
+blp = find_program('blueprint-compiler', required: true)
+sass = find_program('sass', required: true)
+
+dependencies = [
+ dependency('gtk4-layer-shell-0'),
+ dependency('astal-io-0.1'),
+ dependency('glib-2.0'),
+ dependency('astal-4-4.0'),
+ dependency('astal-battery-0.1'),
+ dependency('astal-wireplumber-0.1'),
+ dependency('astal-network-0.1'),
+ dependency('libnm'),
+ dependency('astal-mpris-0.1'),
+ dependency('astal-power-profiles-0.1'),
+ dependency('astal-tray-0.1'),
+ dependency('astal-bluetooth-0.1'),
+]
+
+blueprint_sources = files(
+ 'ui/Bar.blp',
+)
+
+vala_sources = files(
+ 'vala/Bar.vala',
+ 'vala/App.vala',
+)
+
+# transplie blueprints
+ui = custom_target(
+ 'blueprint',
+ input: blueprint_sources,
+ output: '.',
+ command: [
+ blp,
+ 'batch-compile',
+ '@OUTPUT@',
+ '@CURRENT_SOURCE_DIR@',
+ '@INPUT@',
+ ],
+)
+
+# bundle scss files
+css = custom_target(
+ 'scss',
+ input: files('main.scss'),
+ command: [sass, '@INPUT@', '@OUTPUT@'],
+ output: ['main.css'],
+)
+
+# compiling data files into a binary
+resource = import('gnome').compile_resources(
+ 'data',
+ files('gresource.xml'),
+ dependencies: [ui, css],
+ source_dir: meson.current_build_dir(),
+)
+
+executable(
+ meson.project_name(),
+ dependencies: dependencies,
+ sources: [vala_sources, resource],
+ link_args: ['-lm'], # Link math library
+ install: true,
+ install_dir: bindir,
+)
diff --git a/examples/gtk4/simple-bar/vala/src/scss/Bar.scss b/examples/gtk4/simple-bar/vala/src/scss/Bar.scss
new file mode 100644
index 0000000..86ea856
--- /dev/null
+++ b/examples/gtk4/simple-bar/vala/src/scss/Bar.scss
@@ -0,0 +1,19 @@
+// https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-4-16/gtk/theme/Default/_colors-public.scss
+$fg-color: #{"@theme_fg_color"};
+$bg-color: #{"@theme_bg_color"};
+
+window.Bar {
+ > box {
+ background: $bg-color;
+ color: $fg-color;
+ font-weight: bold;
+ }
+
+ button {
+ min-height: 0;
+ min-width: 0;
+ border-radius: 8px;
+ margin: 4px;
+ padding: 4px 8px;
+ }
+}
diff --git a/examples/gtk4/simple-bar/vala/src/ui/Bar.blp b/examples/gtk4/simple-bar/vala/src/ui/Bar.blp
new file mode 100644
index 0000000..6e401e7
--- /dev/null
+++ b/examples/gtk4/simple-bar/vala/src/ui/Bar.blp
@@ -0,0 +1,84 @@
+using Gtk 4.0;
+using Astal 4.0;
+
+template $Bar: Astal.Window {
+ CenterBox centerbox {
+ start-widget: Box {
+ MenuButton {
+ Label {
+ label: bind template.clock;
+ }
+
+ popover: Popover popover {
+ Calendar calendar {
+ show-day-names: true;
+ show-heading: true;
+ show-week-numbers: true;
+ }
+ };
+ }
+ };
+
+ center-widget: Box {
+ Box {
+ visible: bind template.mpris-visible;
+
+ Image {
+ file: bind template.mpris-art;
+ }
+
+ Label {
+ label: bind template.mpris-label;
+ }
+ }
+ };
+
+ end-widget: Box {
+ spacing: 4;
+
+ Image {
+ visible: bind template.bluetooth-visible;
+ icon-name: "bluetooth-symbolic";
+ }
+
+ Image {
+ icon-name: bind template.power-profile-icon;
+ }
+
+ Image {
+ icon-name: bind template.network-icon;
+ }
+
+ Box {
+ Image {
+ icon-name: bind template.volume-icon;
+ }
+
+ Scale {
+ width-request: 100;
+ change-value => $change_volume();
+
+ adjustment: Adjustment {
+ value: bind template.volume;
+ lower: 0;
+ upper: 1;
+ };
+ }
+ }
+
+ Box {
+ Image {
+ icon-name: bind template.battery-icon;
+ }
+
+ Label {
+ label: bind template.battery-label;
+ }
+ }
+
+ Box traybox {
+ spacing: 4;
+ }
+ };
+ }
+}
diff --git a/examples/gtk4/simple-bar/vala/src/vala/App.vala b/examples/gtk4/simple-bar/vala/src/vala/App.vala
new file mode 100644
index 0000000..20be55d
--- /dev/null
+++ b/examples/gtk4/simple-bar/vala/src/vala/App.vala
@@ -0,0 +1,45 @@
+class App : Astal.Application {
+ static App instance;
+
+ // this is where request handlers can be implemented
+ // that will be used to handle `astal` cli invocations
+ public override void request(string request, GLib.SocketConnection conn) {
+ print(@"incoming request: $request\n");
+
+ AstalIO.write_sock.begin(conn, "response", null);
+ }
+
+ // this is the method that will be invoked on `app.run()`
+ // this is where everything should be initialized and instantiated
+ public override void activate() {
+ this.apply_css("resource:///main.css", false);
+ this.add_window(new Bar());
+ }
+
+ // entry point of our app
+ static int main(string[] argv) {
+ App.instance = new App() { instance_name = "simple-bar" };
+
+ try {
+ // `app.acquire_socket()` needed for the request API to work
+ App.instance.acquire_socket();
+
+ // if it succeeds we can run the app
+ return App.instance.run(null);
+ } catch (Error _) {
+ // if it throws an error it means there is already an instance
+ // with `instance_name` running, so we just send a request instead
+ try {
+ var response = AstalIO.send_request(
+ "simple-bar",
+ string.joinv(" ", argv[1:])
+ );
+ print(@"$response\n");
+ return 0;
+ } catch (Error err) {
+ printerr(err.message);
+ return 1;
+ }
+ }
+ }
+}
diff --git a/examples/gtk4/simple-bar/vala/src/vala/Bar.vala b/examples/gtk4/simple-bar/vala/src/vala/Bar.vala
new file mode 100644
index 0000000..010b97e
--- /dev/null
+++ b/examples/gtk4/simple-bar/vala/src/vala/Bar.vala
@@ -0,0 +1,186 @@
+[GtkTemplate(ui="/ui/Bar.ui")]
+class Bar : Astal.Window {
+ public string clock { get; set; }
+ public string volume_icon { get; set; }
+ public string battery_visible { get; set; }
+ public string battery_label { get; set; }
+ public string battery_icon { get; set; }
+ public double volume { get; set; }
+ public string network_icon { get; set; }
+ public bool mpris_visible { get; set; }
+ public string mpris_label { get; set; }
+ public string mpris_art { get; set; }
+ public string power_profile_icon { get; set; }
+ public bool bluetooth_visible { get; set; }
+
+ AstalIO.Time timer;
+ AstalMpris.Player player;
+ HashTable<string, TrayButton> tray_items;
+
+ [GtkChild] unowned Gtk.Popover popover;
+ [GtkChild] unowned Gtk.Calendar calendar;
+ [GtkChild] unowned Gtk.Box traybox;
+
+ public Bar() {
+ anchor = TOP | LEFT | RIGHT;
+ exclusivity = EXCLUSIVE;
+ add_css_class("Bar");
+ present();
+
+ // clock
+ timer = AstalIO.Time.interval(1000, null);
+ timer.now.connect(() => {
+ clock = new DateTime.now_local().format("%H:%M:%S");
+ });
+
+ // everytime popover is opened, select current day
+ popover.notify["visible"].connect(() => {
+ if (popover.visible) {
+ calendar.select_day(new DateTime.now_local());
+ }
+ });
+
+ // network
+ var nw = AstalNetwork.get_default();
+ Binding networkBinding = null;
+
+ nw.bind_property(
+ "primary",
+ this,
+ "network-icon",
+ BindingFlags.SYNC_CREATE,
+ (_, primary) => {
+ if (networkBinding != null) networkBinding.unbind();
+
+ switch (primary.get_enum()) {
+ case AstalNetwork.Primary.WIRED:
+ networkBinding = nw.wired.bind_property(
+ "icon-name",
+ this,
+ "network-icon",
+ BindingFlags.SYNC_CREATE
+ );
+ return false;
+
+ case AstalNetwork.Primary.WIFI:
+ networkBinding = nw.wifi.bind_property(
+ "icon-name",
+ this,
+ "network-icon",
+ BindingFlags.SYNC_CREATE
+ );
+ return false;
+
+ default:
+ network_icon = "network-idle-symbolic";
+ return false;
+ }
+ },
+ null
+ );
+
+ // battery
+ var bat = AstalBattery.get_default();
+ bat.bind_property("is-present", this, "battery-visible", BindingFlags.SYNC_CREATE);
+ bat.bind_property("icon-name", this, "battery-icon", BindingFlags.SYNC_CREATE);
+ bat.bind_property("percentage", this, "battery-label", BindingFlags.SYNC_CREATE, (_, src, ref target) => {
+ target.set_string(@"$(Math.floor(bat.percentage * 100))%");
+ return true;
+ }, null);
+
+ // volume
+ var speaker = AstalWp.get_default().get_default_speaker();
+ speaker.bind_property("volume-icon", this, "volume-icon", BindingFlags.SYNC_CREATE);
+ speaker.bind_property("volume", this, "volume", BindingFlags.SYNC_CREATE);
+
+ // mpris
+ player = new AstalMpris.Player("spotify");
+ player.bind_property("available", this, "mpris-visible", BindingFlags.SYNC_CREATE);
+ player.bind_property("cover-art", this, "mpris-art", BindingFlags.SYNC_CREATE);
+ player.bind_property("metadata", this, "mpris-label", BindingFlags.SYNC_CREATE, (_, src, ref target) => {
+ if (player.title == null || player.artist == null) {
+ return false;
+ }
+ target.set_string(@"$(player.artist) - $(player.title)");
+ return true;
+ }, null);
+
+ // powerprofiles
+ var powerprofile = AstalPowerProfiles.get_default();
+ powerprofile.bind_property("icon-name", this, "power-profile-icon", BindingFlags.SYNC_CREATE);
+
+ // tray
+ var tray = AstalTray.get_default();
+ tray_items = new HashTable<string, TrayButton>(str_hash, str_equal);
+ tray.item_added.connect(on_tray_item_added);
+ tray.item_removed.connect(on_tray_item_removed);
+
+ // bluetooth
+ var bt = AstalBluetooth.get_default();
+ bt.bind_property("is-connected", this, "bluetooth-visible", BindingFlags.SYNC_CREATE);
+ }
+
+ void on_tray_item_added(AstalTray.Tray tray, string id) {
+ var button = new TrayButton(id);
+ tray_items.set(id, button);
+ traybox.append(button);
+ }
+
+ void on_tray_item_removed(string id) {
+ var button = tray_items.get(id);
+ traybox.remove(button);
+ tray_items.remove(id);
+ }
+
+ [GtkCallback]
+ bool change_volume(Gtk.Range scale, Gtk.ScrollType type, double value) {
+ AstalWp.get_default().get_default_speaker().set_volume(value);
+ return true;
+ }
+
+ public override void dispose() {
+ var tray = AstalTray.get_default();
+ tray.item_added.disconnect(on_tray_item_added);
+ tray.item_removed.disconnect(on_tray_item_removed);
+
+ foreach (var button in tray_items.get_values()) {
+ button.dispose();
+ }
+
+ timer.cancel();
+ timer.dispose();
+ player.dispose();
+ base.dispose();
+ }
+
+ class TrayButton : Astal.Bin {
+ AstalTray.TrayItem item;
+ Gtk.Popover popover;
+ Gtk.Image image;
+
+ public TrayButton(string id) {
+ var tray = AstalTray.get_default();
+ item = tray.get_item(id);
+
+ image = new Gtk.Image();
+ popover = new Gtk.PopoverMenu.from_model(item.menu_model);
+
+ child = new Gtk.MenuButton() {
+ child = image,
+ popover = popover,
+ };
+
+ item.bind_property("gicon", image, "gicon", BindingFlags.SYNC_CREATE);
+ popover.insert_action_group("dbusmenu", item.action_group);
+ item.notify["action-group"].connect(on_action_group);
+ }
+
+ void on_action_group() {
+ popover.insert_action_group("dbusmenu", item.action_group);
+ }
+
+ public override void dispose() {
+ item.notify.disconnect(on_action_group);
+ }
+ }
+}