diff options
Diffstat (limited to 'examples/gtk4/simple-bar/py')
-rw-r--r-- | examples/gtk4/simple-bar/py/.gitignore | 3 | ||||
-rw-r--r-- | examples/gtk4/simple-bar/py/README.md | 45 | ||||
-rw-r--r-- | examples/gtk4/simple-bar/py/flake.nix | 55 | ||||
-rw-r--r-- | examples/gtk4/simple-bar/py/meson.build | 12 | ||||
-rw-r--r-- | examples/gtk4/simple-bar/py/src/gresource.xml | 7 | ||||
-rwxr-xr-x | examples/gtk4/simple-bar/py/src/main.in.py | 19 | ||||
-rw-r--r-- | examples/gtk4/simple-bar/py/src/main.scss | 1 | ||||
-rw-r--r-- | examples/gtk4/simple-bar/py/src/meson.build | 59 | ||||
-rw-r--r-- | examples/gtk4/simple-bar/py/src/py/App.py | 41 | ||||
-rw-r--r-- | examples/gtk4/simple-bar/py/src/py/Bar.py | 184 | ||||
-rw-r--r-- | examples/gtk4/simple-bar/py/src/py/__init__.py | 15 | ||||
-rw-r--r-- | examples/gtk4/simple-bar/py/src/scss/Bar.scss | 19 | ||||
-rw-r--r-- | examples/gtk4/simple-bar/py/src/ui/Bar.blp | 84 |
13 files changed, 544 insertions, 0 deletions
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; + } + }; + } +} |