From 23cdbc8088b5c308a068b432a6b03213ede68f07 Mon Sep 17 00:00:00 2001 From: Aylur Date: Sat, 1 Mar 2025 20:59:09 +0100 Subject: add gtk4 examples --- examples/gtk4/simple-bar/js/.gitignore | 5 + examples/gtk4/simple-bar/js/README.md | 53 ++++++++ examples/gtk4/simple-bar/js/flake.nix | 52 +++++++ examples/gtk4/simple-bar/js/meson.build | 12 ++ examples/gtk4/simple-bar/js/src/gresource.xml | 8 ++ examples/gtk4/simple-bar/js/src/main.in.js | 12 ++ examples/gtk4/simple-bar/js/src/main.in.sh | 3 + examples/gtk4/simple-bar/js/src/main.scss | 1 + examples/gtk4/simple-bar/js/src/meson.build | 84 ++++++++++++ examples/gtk4/simple-bar/js/src/scss/Bar.scss | 19 +++ examples/gtk4/simple-bar/js/src/ts/App.ts | 52 +++++++ examples/gtk4/simple-bar/js/src/ts/Bar.ts | 189 ++++++++++++++++++++++++++ examples/gtk4/simple-bar/js/src/ts/props.ts | 45 ++++++ examples/gtk4/simple-bar/js/src/ui/Bar.blp | 84 ++++++++++++ examples/gtk4/simple-bar/js/tsconfig.json | 8 ++ 15 files changed, 627 insertions(+) create mode 100644 examples/gtk4/simple-bar/js/.gitignore create mode 100644 examples/gtk4/simple-bar/js/README.md create mode 100644 examples/gtk4/simple-bar/js/flake.nix create mode 100644 examples/gtk4/simple-bar/js/meson.build create mode 100644 examples/gtk4/simple-bar/js/src/gresource.xml create mode 100755 examples/gtk4/simple-bar/js/src/main.in.js create mode 100755 examples/gtk4/simple-bar/js/src/main.in.sh create mode 100644 examples/gtk4/simple-bar/js/src/main.scss create mode 100644 examples/gtk4/simple-bar/js/src/meson.build create mode 100644 examples/gtk4/simple-bar/js/src/scss/Bar.scss create mode 100644 examples/gtk4/simple-bar/js/src/ts/App.ts create mode 100644 examples/gtk4/simple-bar/js/src/ts/Bar.ts create mode 100644 examples/gtk4/simple-bar/js/src/ts/props.ts create mode 100644 examples/gtk4/simple-bar/js/src/ui/Bar.blp create mode 100644 examples/gtk4/simple-bar/js/tsconfig.json (limited to 'examples/gtk4/simple-bar/js') 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 @@ + + + + index.js + main.css + ui/Bar.ui + + 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 { + 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() + 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" + } +} -- cgit v1.2.3