summaryrefslogtreecommitdiff
path: root/examples/gtk4/simple-bar/js/src
diff options
context:
space:
mode:
authorAylur <[email protected]>2025-03-01 20:59:09 +0100
committerAylur <[email protected]>2025-03-01 21:02:29 +0100
commit23cdbc8088b5c308a068b432a6b03213ede68f07 (patch)
tree1e8bd6ffde5273fcd80aca0d30cbb38dbe5f9461 /examples/gtk4/simple-bar/js/src
parentdfd1f23c7562694e571d44c45aa74fcea9b1ba01 (diff)
add gtk4 examples
Diffstat (limited to 'examples/gtk4/simple-bar/js/src')
-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
10 files changed, 497 insertions, 0 deletions
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;
+ }
+ };
+ }
+}