diff options
author | Aylur <[email protected]> | 2024-05-19 02:39:53 +0200 |
---|---|---|
committer | Aylur <[email protected]> | 2024-05-19 02:39:53 +0200 |
commit | 1425b396b08f0e91d45bbd0f92b1309115c7c870 (patch) | |
tree | 8af1a899a14d8a01a9ef50e248c077b48aed25bc /python |
init 0.1.0
Diffstat (limited to 'python')
-rw-r--r-- | python/.gitignore | 1 | ||||
-rw-r--r-- | python/astal/__init__.py | 16 | ||||
-rw-r--r-- | python/astal/application.py | 62 | ||||
-rw-r--r-- | python/astal/binding.py | 33 | ||||
-rw-r--r-- | python/astal/variable.py | 100 | ||||
-rw-r--r-- | python/astal/widget.py | 65 | ||||
-rw-r--r-- | python/pyproject.toml | 14 | ||||
-rw-r--r-- | python/ruff.toml | 62 | ||||
-rwxr-xr-x | python/sample.py | 31 |
9 files changed, 384 insertions, 0 deletions
diff --git a/python/.gitignore b/python/.gitignore new file mode 100644 index 0000000..c18dd8d --- /dev/null +++ b/python/.gitignore @@ -0,0 +1 @@ +__pycache__/ diff --git a/python/astal/__init__.py b/python/astal/__init__.py new file mode 100644 index 0000000..c679c4a --- /dev/null +++ b/python/astal/__init__.py @@ -0,0 +1,16 @@ +import gi + +gi.require_version("Astal", "0.1") +gi.require_version("Gtk", "3.0") +gi.require_version("GLib", "2.0") +gi.require_version("Gio", "2.0") +gi.require_version("GObject", "2.0") +from gi.repository import Astal, Gtk, GLib, Gio, GObject +from .application import App +from .variable import Variable +from .binding import Binding +from . import widget as Widget + +bind = Binding + +__all__ = ["App", "Variable", "Widget" "bind", "Astal", "Gtk", "GLib", "Gio", "GObject"] diff --git a/python/astal/application.py b/python/astal/application.py new file mode 100644 index 0000000..3eb1a4f --- /dev/null +++ b/python/astal/application.py @@ -0,0 +1,62 @@ +from collections.abc import Callable +from gi.repository import Astal, Gio + +RequestHandler = Callable[[str, Callable[[str], None]], None] + + +class _Application(Astal.Application): + def __init__(self) -> None: + super().__init__() + self.request_handler: RequestHandler | None = None + + def do_response(self, msg: str, conn: Gio.SocketConnection) -> None: + if self.request_handler: + self.request_handler( + msg, + lambda response: Astal.write_sock( + conn, + response, + lambda _, res: Astal.write_sock_finish(res), + ), + ) + else: + super().do_response(msg, conn) + + def start( + self, + instance_name: str | None = None, + gtk_theme: str | None = None, + icon_theme: str | None = None, + cursor_theme: str | None = None, + css: str | None = None, + hold: bool | None = True, + request_handler: RequestHandler | None = None, + callback: Callable | None = None, + ) -> None: + if request_handler: + self.request_handler = request_handler + if hold: + self.hold() + if instance_name: + self.instance_name = instance_name + if gtk_theme: + self.gtk_theme = gtk_theme + if icon_theme: + self.icon_theme = icon_theme + if cursor_theme: + self.cursor_theme = icon_theme + if css: + self.apply_css(css, False) + if not self.acquire_socket(): + print(f"Astal instance {self.instance_name} already running") + return + + def on_activate(app): + if callback: + callback() + + self.connect("activate", on_activate) + self.run() + + +App = _Application() diff --git a/python/astal/binding.py b/python/astal/binding.py new file mode 100644 index 0000000..0fe1b6c --- /dev/null +++ b/python/astal/binding.py @@ -0,0 +1,33 @@ +import re + + +def kebabify(string): + return re.sub(r"([a-z])([A-Z])", r"\1-\2", string).replace("_", "-").lower() + + +class Binding: + def __init__(self, emitter, prop=None): + self.emitter = emitter + self.prop = kebabify(prop) if prop else None + self.transform_fn = lambda v: v + + def __str__(self): + return f"Binding<{self.emitter}{', ' + self.prop if self.prop else ''}>" + + def as_(self, fn): + bind = Binding(self.emitter, self.prop) + bind.transform_fn = lambda v: fn(self.transform_fn(v)) + return bind + + def get(self): + if hasattr(self.emitter, "get") and callable(self.emitter.get): + return self.transform_fn(self.emitter.get()) + + return self.transform_fn(self.emitter[f"get_{self.prop}"]()) + + def subscribe(self, callback): + if hasattr(self.emitter, "subscribe") and callable(self.emitter.subscribe): + return self.emitter.subscribe(lambda _: callback(self.get())) + + i = self.emitter.connect(f"notify::{self.prop}", lambda: callback(self.get())) + return lambda: self.emitter.disconnect(i) diff --git a/python/astal/variable.py b/python/astal/variable.py new file mode 100644 index 0000000..3b6a71d --- /dev/null +++ b/python/astal/variable.py @@ -0,0 +1,100 @@ +from gi.repository import Astal + +from .binding import Binding + + +class Variable: + def __init__(self, init): + v = Astal.Variable.new(init) + self._variable = v + self._err_handler = print + v.connect("error", lambda _, err: self._err_handler(err) if self._err_handler else None) + + def __call__(self, transform=None): + if transform: + return Binding(self).as_(transform) + + return Binding(self) + + def __str__(self): + return f"Variable<{self.get()}>" + + def get(self): + return self._variable.get_value() + + def set(self, value): + return self._variable.set_value(value) + + def watch(self, cmd): + if isinstance(cmd, str): + self._variable.watch(cmd) + elif isinstance(cmd, list): + self._variable.watchv(cmd) + return self + + def poll(self, interval, cmd): + if isinstance(cmd, str): + self._variable.poll(interval, cmd) + elif isinstance(cmd, list): + self._variable.pollv(interval, cmd) + else: + self._variable.pollfn(interval, cmd) + return self + + def start_watch(self): + self._variable.start_watch() + + def start_poll(self): + self._variable.start_poll() + + def stop_watch(self): + self._variable.stop_watch() + + def stop_poll(self): + self._variable.stop_poll() + + def drop(self): + self._variable.emit_dropped() + self._variable.run_dispose() + + def on_dropped(self, callback): + self._variable.connect("dropped", lambda _: callback()) + return self + + def on_error(self, callback): + self._err_handler = None + self._variable.connect("error", lambda _, e: callback(e)) + return self + + def subscribe(self, callback): + s = self._variable.connect("changed", lambda _: callback(self.get())) + return lambda: self._variable.disconnect(s) + + def observe(self, objs, sigOrFn, callback=None): + if callable(sigOrFn): + f = sigOrFn + elif callable(callback): + f = callback + else: + f = lambda *_: self.get() + + def setter(_, *args): + self.set(f(*args)) + + if isinstance(objs, list): + for obj in objs: + obj[0].connect(obj[1], setter) + elif isinstance(sigOrFn, str): + objs.connect(sigOrFn, setter) + + return self + + @staticmethod + def derive(deps, fn): + def update(): + return fn(*[d.get() for d in deps]) + + derived = Variable(update()) + unsubs = [dep.subscribe(lambda _: derived.set(update())) for dep in deps] + derived.on_dropped(lambda: ([unsub() for unsub in unsubs])) + return derived diff --git a/python/astal/widget.py b/python/astal/widget.py new file mode 100644 index 0000000..7070e8f --- /dev/null +++ b/python/astal/widget.py @@ -0,0 +1,65 @@ +from gi.repository import Astal, Gtk +from .binding import Binding + + +def set_child(self, child): + if isinstance(self, Gtk.Bin): + self.remove(self.get_child()) + if isinstance(self, Gtk.Container): + self.add(child) + + +def astalify(ctor): + ctor.set_css = Astal.widget_set_css + ctor.get_css = Astal.widget_get_css + + ctor.set_class_name = lambda self, names: Astal.widget_set_class_names(self, names.split()) + ctor.get_class_name = lambda self: " ".join(Astal.widget_set_class_names(self)) + + ctor.set_cursor = Astal.widget_set_cursor + ctor.get_cursor = Astal.widget_get_cursor + + def widget(**kwargs): + args = {} + bindings = {} + handlers = {} + setup = None + if not hasattr(kwargs, "visible"): + kwargs["visible"] = True + + for key, value in kwargs.items(): + if key == "setup": + setup = value + if isinstance(value, Binding): + bindings[key] = value + if key.startswith("on_"): + handlers[key] = value + else: + args[key] = value + + self = ctor(**args) + + for key, value in bindings.items(): + setter = getattr(self, f"set_{key}") + setter(value.get()) + unsub = value.subscribe(setter) + self.connect("destroy", lambda _: unsub()) + + for key, value in handlers.items(): + self.connect(key.replace("on_", ""), value) + + if setup: + setup(self) + + return self + + return widget + + +Window = astalify(Astal.Window) +Box = astalify(Astal.Box) +Button = astalify(Astal.Button) +CenterBox = astalify(Astal.CenterBox) +Label = astalify(Gtk.Label) +Icon = astalify(Astal.Icon) +EventBox = astalify(Astal.EventBox) diff --git a/python/pyproject.toml b/python/pyproject.toml new file mode 100644 index 0000000..2103286 --- /dev/null +++ b/python/pyproject.toml @@ -0,0 +1,14 @@ +[tool.poetry] +name = "astal.py" +version = "0.1.0" +description = "" +authors = [] + +[tool.poetry.dependencies] +python = "^3.11" +gengir = "^1.0.2" + + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/python/ruff.toml b/python/ruff.toml new file mode 100644 index 0000000..a6bedc2 --- /dev/null +++ b/python/ruff.toml @@ -0,0 +1,62 @@ +target-version = "py311" + +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "site-packages", + "venv", +] + +line-length = 100 +indent-width = 4 + +[lint] +select = ["ALL"] +ignore = ["D", "ANN101", "ERA", "ANN"] + +# Allow fix for all enabled rules (when `--fix`) is provided. +fixable = ["ALL"] +unfixable = [] + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +[format] +quote-style = "double" +indent-style = "space" + +# Like Black, respect magic trailing commas. +skip-magic-trailing-comma = false + +# Like Black, automatically detect the appropriate line ending. +line-ending = "auto" + +# Enable auto-formatting of code examples in docstrings. Markdown, +# reStructuredText code/literal blocks and doctests are all supported. +docstring-code-format = false + +# Set the line length limit used when formatting code snippets in +# docstrings. +docstring-code-line-length = "dynamic" diff --git a/python/sample.py b/python/sample.py new file mode 100755 index 0000000..af09ce2 --- /dev/null +++ b/python/sample.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +import gi + +gi.require_version("Playerctl", "2.0") + +from gi.repository import Playerctl +from astal import App, Astal, Variable, Widget, bind + +player = Playerctl.Player.new("spotify") +v = Variable(player.get_title()).observe(player, "metadata", lambda *_: player.get_title()) + + +def Bar(monitor): + return Widget.Window( + anchor=Astal.WindowAnchor.BOTTOM | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.RIGHT, + monitor=monitor, + exclusivity=Astal.Exclusivity.EXCLUSIVE, + child=Widget.CenterBox( + start_widget=Widget.Label( + label="Welcome to Astal.py!", + ), + end_widget=Widget.Label(label=v()), + ), + ) + + +def start(): + Bar(0) + + +App.start(callback=start) |