mirror of
https://github.com/zoriya/astal.git
synced 2025-12-06 06:06:10 +00:00
feat: python
A python library similar to the gjs and lua one. This is moved from the astal-sh/libastal repo.
This commit is contained in:
1
core/python/.gitignore
vendored
Normal file
1
core/python/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
__pycache__/
|
||||
17
core/python/astal/__init__.py
Normal file
17
core/python/astal/__init__.py
Normal file
@@ -0,0 +1,17 @@
|
||||
import gi
|
||||
|
||||
gi.require_version("Astal", "0.1")
|
||||
gi.require_version("Gtk", "3.0")
|
||||
gi.require_version("Gdk", "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, Gdk
|
||||
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", "Gdk", "GLib", "Gio", "GObject"]
|
||||
62
core/python/astal/application.py
Normal file
62
core/python/astal/application.py
Normal file
@@ -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_request(self, msg: str, conn: Gio.SocketConnection) -> None:
|
||||
if self.request_handler:
|
||||
self.request_handler(
|
||||
msg,
|
||||
lambda response: Astal.write_sock(
|
||||
conn,
|
||||
str(response),
|
||||
lambda _, res: Astal.write_sock_finish(res),
|
||||
),
|
||||
)
|
||||
else:
|
||||
super().do_request(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()
|
||||
33
core/python/astal/binding.py
Normal file
33
core/python/astal/binding.py
Normal file
@@ -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)
|
||||
100
core/python/astal/variable.py
Normal file
100
core/python/astal/variable.py
Normal file
@@ -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
|
||||
78
core/python/astal/widget.py
Normal file
78
core/python/astal/widget.py
Normal file
@@ -0,0 +1,78 @@
|
||||
from gi.repository import Astal, Gtk
|
||||
from .binding import Binding, kebabify
|
||||
|
||||
|
||||
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(kebabify(key.replace("on_", "")), value)
|
||||
|
||||
if setup:
|
||||
setup(self)
|
||||
|
||||
return self
|
||||
|
||||
return widget
|
||||
|
||||
|
||||
Box = astalify(Astal.Box),
|
||||
Button = astalify(Astal.Button),
|
||||
CenterBox = astalify(Astal.CenterBox),
|
||||
# TODO: CircularProgress
|
||||
DrawingArea = astalify(Gtk.DrawingArea),
|
||||
Entry = astalify(Gtk.Entry),
|
||||
EventBox = astalify(Astal.EventBox),
|
||||
# TODO: Fixed
|
||||
# TODO: FlowBox
|
||||
Icon = astalify(Astal.Icon),
|
||||
Label = astalify(Gtk.Label),
|
||||
LevelBar = astalify(Astal.LevelBar),
|
||||
# TODO: ListBox
|
||||
Overlay = astalify(Astal.Overlay),
|
||||
Revealer = astalify(Gtk.Revealer),
|
||||
Scrollable = astalify(Astal.Scrollable),
|
||||
Slider = astalify(Astal.Slider),
|
||||
# TODO: Stack
|
||||
Switch = astalify(Gtk.Switch),
|
||||
Window = astalify(Astal.Window),
|
||||
14
core/python/pyproject.toml
Normal file
14
core/python/pyproject.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[tool.poetry]
|
||||
name = "astal"
|
||||
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"
|
||||
62
core/python/ruff.toml
Normal file
62
core/python/ruff.toml
Normal file
@@ -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"
|
||||
31
core/python/sample.py
Executable file
31
core/python/sample.py
Executable file
@@ -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)
|
||||
Reference in New Issue
Block a user