summaryrefslogtreecommitdiff
path: root/examples/gtk4/simple-bar/js
diff options
context:
space:
mode:
Diffstat (limited to 'examples/gtk4/simple-bar/js')
-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
15 files changed, 627 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"
+ }
+}