rewrite ui in hy, with python astal bindings
This commit is contained in:
parent
afc9ff29e7
commit
ad3e377963
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
__pycache__
|
||||
style/style.css
|
7
astal/__init__.py
Normal file
7
astal/__init__.py
Normal file
@ -0,0 +1,7 @@
|
||||
import gi
|
||||
|
||||
from .binding import bind
|
||||
from .variable import Variable
|
||||
from .file import *
|
||||
from .time import *
|
||||
from .process import *
|
33
astal/binding.py
Normal file
33
astal/binding.py
Normal file
@ -0,0 +1,33 @@
|
||||
import gi
|
||||
|
||||
gi.require_version("GObject", "2.0")
|
||||
|
||||
from gi.repository import GObject
|
||||
|
||||
# from variable import Variable
|
||||
|
||||
class Binding(GObject.Object):
|
||||
def __init__(self, emitter: GObject.GObject, property: str | None = None, transform_fn = lambda x: x):
|
||||
self.emitter = emitter
|
||||
self.property = property
|
||||
self.transform_fn = transform_fn
|
||||
|
||||
def get(self):
|
||||
return self.transform_fn(self.emitter.get_property(self.property) if self.property else self.emitter.get())
|
||||
|
||||
def transform(self, fn):
|
||||
return Binding(self.emitter, self.property, lambda x: fn(self.transform_fn(x)))
|
||||
|
||||
def subscribe(self, callback):
|
||||
id = self.emitter.connect(
|
||||
f'notify::{self.property}' if self.property else 'changed',
|
||||
lambda gobject, _=None: callback(self.transform_fn(self.emitter.get_property(self.property) if self.property else self.emitter.get()))
|
||||
)
|
||||
|
||||
def unsubscribe(_=None):
|
||||
self.emitter.disconnect(id)
|
||||
|
||||
return unsubscribe
|
||||
|
||||
def bind(*args, **kwargs):
|
||||
return Binding(*args, **kwargs)
|
30
astal/file.py
Normal file
30
astal/file.py
Normal file
@ -0,0 +1,30 @@
|
||||
import gi
|
||||
from typing import Callable
|
||||
|
||||
gi.require_version("AstalIO", "0.1")
|
||||
gi.require_version("GObject", "2.0")
|
||||
|
||||
from gi.repository import AstalIO, GObject
|
||||
|
||||
def read_file(fp: str) -> str:
|
||||
return AstalIO.read_file(fp)
|
||||
|
||||
def read_file_async(fp: str, callback: Callable[[str | None, Exception | None], None]) -> None:
|
||||
try:
|
||||
AstalIO.read_file_async(fp, lambda _, res: callback(AstalIO.read_file_finish(res), None))
|
||||
|
||||
except Exception as e:
|
||||
callback(None, e)
|
||||
|
||||
def write_file(fp: str, content: str) -> None:
|
||||
AstalIO.write_file(fp, content)
|
||||
|
||||
def write_file_async(fp: str, content: str, callback: Callable[[Exception], None]) -> None:
|
||||
try:
|
||||
AstalIO.write_file_async(fp, content, lambda _, res: AstalIO.write_file_finish(res))
|
||||
|
||||
except Exception as e:
|
||||
callback(e)
|
||||
|
||||
def monitor_file(fp: str, callback: Callable[[str, int], None]) -> None:
|
||||
return AstalIO.monitor_file(fp, callback)
|
12
astal/gtk3/__init__.py
Normal file
12
astal/gtk3/__init__.py
Normal file
@ -0,0 +1,12 @@
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
gi.require_version("Gdk", "3.0")
|
||||
gi.require_version("Astal", "3.0")
|
||||
|
||||
from .app import App
|
||||
from .astalify import astalify
|
||||
from .widget import Widget
|
||||
|
||||
from gi.repository import Gtk, Gdk, Astal
|
||||
|
62
astal/gtk3/app.py
Normal file
62
astal/gtk3/app.py
Normal file
@ -0,0 +1,62 @@
|
||||
import gi, sys
|
||||
|
||||
from typing import Callable, Optional, Any, Dict
|
||||
|
||||
gi.require_version("Astal", "3.0")
|
||||
gi.require_version("AstalIO", "0.1")
|
||||
from gi.repository import Astal, AstalIO, Gio
|
||||
|
||||
type Config = Dict[str, Any]
|
||||
|
||||
class AstalPy(Astal.Application):
|
||||
request_handler: Optional[Callable[[str, Callable[[Any], None]], None]] = None
|
||||
|
||||
def do_astal_application_request(self, msg: str, conn: Gio.SocketConnection):
|
||||
print(msg)
|
||||
if callable(self.request_handler):
|
||||
def respond(response: Any):
|
||||
AstalIO.write_sock(conn, str(response), None, None)
|
||||
self.request_handler(msg, respond)
|
||||
else:
|
||||
AstalIO.Application.do_request(self, msg, conn)
|
||||
|
||||
def quit(self, code: int = 0):
|
||||
super().quit()
|
||||
sys.exit(code)
|
||||
|
||||
def apply_css(self, css: str, reset: bool = False):
|
||||
super().apply_css(css, reset)
|
||||
|
||||
def start(self, **config: Any):
|
||||
config.setdefault("client", lambda *_: (print(f'Astal instance "{self.get_instance_name()}" is already running'), sys.exit(1)))
|
||||
config.setdefault("hold", True)
|
||||
|
||||
self.request_handler = config.get("request_handler")
|
||||
|
||||
if "css" in config:
|
||||
self.apply_css(config["css"])
|
||||
if "icons" in config:
|
||||
self.add_icons(config["icons"])
|
||||
|
||||
for key in ["instance_name", "gtk_theme", "icon_theme", "cursor_theme"]:
|
||||
if key in config:
|
||||
self.set_property(key, config[key])
|
||||
|
||||
def on_activate(_):
|
||||
if callable(config.get("main")):
|
||||
config["main"]()
|
||||
if config["hold"]:
|
||||
self.hold()
|
||||
|
||||
self.connect("activate", on_activate)
|
||||
|
||||
try:
|
||||
self.acquire_socket()
|
||||
except Exception:
|
||||
return config["client"](lambda msg: AstalIO.send_message(self.get_instance_name(), msg), *sys.argv[1:])
|
||||
|
||||
self.run()
|
||||
|
||||
return self
|
||||
|
||||
App = AstalPy()
|
111
astal/gtk3/astalify.py
Normal file
111
astal/gtk3/astalify.py
Normal file
@ -0,0 +1,111 @@
|
||||
import gi
|
||||
from functools import partial, singledispatch
|
||||
|
||||
from typing import Callable, List
|
||||
|
||||
from astal.binding import Binding, bind
|
||||
from astal.variable import Variable
|
||||
|
||||
gi.require_version("Astal", "3.0")
|
||||
gi.require_version("AstalIO", "0.1")
|
||||
gi.require_version("Gtk", "3.0")
|
||||
gi.require_version("GObject", "2.0")
|
||||
from gi.repository import Astal, Gtk, GObject
|
||||
|
||||
def astalify(widget: Gtk.Widget):
|
||||
class Widget(widget):
|
||||
class_name = ''
|
||||
|
||||
def hook(self, object: GObject.Object, signal_or_callback: str | Callable, callback: Callable = lambda _, x: x):
|
||||
if isinstance(signal_or_callback, Callable):
|
||||
callback = signal_or_callback
|
||||
|
||||
if isinstance(object, Variable):
|
||||
unsubscribe = object.subscribe(callback)
|
||||
|
||||
else:
|
||||
if isinstance(signal_or_callback, Callable): return
|
||||
|
||||
if 'notify::' in signal_or_callback:
|
||||
id = object.connect(f'{signal_or_callback}', lambda obj, *_: callback(self, object.get_property(signal_or_callback.replace('notify::', '').replace('-', '_')) if signal_or_callback.replace('notify::', '') in [*map(lambda x: x.name, object.list_properties())] else None))
|
||||
|
||||
else:
|
||||
id = object.connect(signal_or_callback, lambda _, value, *args: callback(self, value) if not args else callback(self, value, *args))
|
||||
|
||||
unsubscribe = lambda _=None: object.disconnect(id)
|
||||
|
||||
self.connect('destroy', unsubscribe)
|
||||
|
||||
def toggle_class_name(self, name: str, state: bool | None = None):
|
||||
Astal.widget_toggle_class_name(self, name, state if state is not None else not name in Astal.widget_get_class_names(self))
|
||||
|
||||
@GObject.Property(type=str)
|
||||
def class_name(self):
|
||||
return ' '.join(Astal.widget_get_class_names(self))
|
||||
|
||||
@class_name.setter
|
||||
def class_name(self, name):
|
||||
Astal.widget_set_class_names(self, name.split(' '))
|
||||
|
||||
@GObject.Property(type=str)
|
||||
def css(self):
|
||||
return Astal.widget_get_css(self)
|
||||
|
||||
@css.setter
|
||||
def css(self, css: str):
|
||||
Astal.widget_set_css(self, css)
|
||||
|
||||
@GObject.Property(type=str)
|
||||
def cursor(self):
|
||||
return Astal.widget_get_cursor(self)
|
||||
|
||||
@cursor.setter
|
||||
def cursor(self, cursor: str):
|
||||
Astal.widget_set_cursor(self, cursor)
|
||||
|
||||
@GObject.Property(type=str)
|
||||
def click_through(self):
|
||||
return Astal.widget_get_click_through(self)
|
||||
|
||||
@click_through.setter
|
||||
def click_through(self, click_through: str):
|
||||
Astal.widget_set_click_through(self, click_through)
|
||||
|
||||
if widget == Astal.Box or widget == Gtk.Box:
|
||||
@GObject.Property()
|
||||
def children(self):
|
||||
return Astal.Box.get_children(self)
|
||||
|
||||
@children.setter
|
||||
def children(self, children):
|
||||
Astal.Box.set_children(self, children)
|
||||
|
||||
def __init__(self, **props):
|
||||
super().__init__()
|
||||
|
||||
if not 'show' in props:
|
||||
self.show()
|
||||
|
||||
else:
|
||||
if props['show']:
|
||||
self.show()
|
||||
|
||||
for prop, value in props.items():
|
||||
if isinstance(value, Binding):
|
||||
self.set_property(prop, value.get())
|
||||
unsubscribe = value.subscribe(partial(self.set_property, prop))
|
||||
self.connect('destroy', unsubscribe)
|
||||
|
||||
elif 'on_' == prop[0:3] and isinstance(value, Callable):
|
||||
self.connect(prop.replace('on_', '', 1), value)
|
||||
|
||||
elif prop.replace('_', '-') in map(lambda x: x.name, self.props):
|
||||
self.set_property(prop.replace('_', '-'), value)
|
||||
|
||||
elif prop == 'setup' and isinstance(value, Callable):
|
||||
value(self)
|
||||
|
||||
else:
|
||||
self.__setattr__(prop, value)
|
||||
|
||||
return Widget
|
33
astal/gtk3/widget.py
Normal file
33
astal/gtk3/widget.py
Normal file
@ -0,0 +1,33 @@
|
||||
import gi
|
||||
|
||||
from .astalify import astalify
|
||||
from enum import Enum
|
||||
|
||||
gi.require_version("Astal", "3.0")
|
||||
gi.require_version("Gtk", "3.0")
|
||||
|
||||
from gi.repository import Astal, Gtk
|
||||
|
||||
class CallableEnum(Enum):
|
||||
def __call__(self, *args, **kwargs):
|
||||
return self.value(*args, **kwargs)
|
||||
|
||||
class Widget(CallableEnum):
|
||||
Box = astalify(Astal.Box)
|
||||
Button = astalify(Astal.Button)
|
||||
CenterBox = astalify(Astal.CenterBox)
|
||||
CircularProgress = astalify(Astal.CircularProgress)
|
||||
DrawingArea = astalify(Gtk.DrawingArea)
|
||||
Entry = astalify(Gtk.Entry)
|
||||
EventBox = astalify(Astal.EventBox)
|
||||
Icon = astalify(Astal.Icon)
|
||||
Label = astalify(Gtk.Label)
|
||||
LevelBar = astalify(Astal.LevelBar)
|
||||
MenuButton = astalify(Gtk.MenuButton)
|
||||
Overlay = astalify(Astal.Overlay)
|
||||
Revealer = astalify(Gtk.Revealer)
|
||||
Scrollable = astalify(Astal.Scrollable)
|
||||
Slider = astalify(Astal.Slider)
|
||||
Stack = astalify(Astal.Stack)
|
||||
Switch = astalify(Gtk.Switch)
|
||||
Window = astalify(Astal.Window)
|
48
astal/process.py
Normal file
48
astal/process.py
Normal file
@ -0,0 +1,48 @@
|
||||
import gi, sys
|
||||
|
||||
from typing import List, Callable
|
||||
|
||||
gi.require_version("AstalIO", "0.1")
|
||||
|
||||
from gi.repository import AstalIO
|
||||
|
||||
def subprocess(command: str | List[str], output=None, error=None) -> AstalIO.Process | None:
|
||||
if not output:
|
||||
output = lambda proc, x: sys.stdout.write(x + '\n')
|
||||
|
||||
if not error:
|
||||
error = lambda proc, x: sys.stderr.write(x + '\n')
|
||||
|
||||
if isinstance(command, list):
|
||||
proc = AstalIO.Process.subprocessv(command)
|
||||
|
||||
else:
|
||||
proc = AstalIO.Process.subprocess(command)
|
||||
|
||||
proc.connect('stdout', output)
|
||||
proc.connect('stderr', error)
|
||||
|
||||
return proc
|
||||
|
||||
def exec(command: str | List[str]) -> str:
|
||||
if isinstance(command, list):
|
||||
return AstalIO.Process.execv(command)
|
||||
|
||||
else:
|
||||
return AstalIO.Process.exec(command)
|
||||
|
||||
def exec_async(command: str | List[str], callback: Callable[[str, str], None] | None = None) -> None:
|
||||
def default_callback(output, error=None):
|
||||
if error:
|
||||
sys.stderr.write(error + '\n')
|
||||
|
||||
else: sys.stdout.write(output + '\n')
|
||||
|
||||
if not callback:
|
||||
callback = default_callback
|
||||
|
||||
if isinstance(command, list):
|
||||
AstalIO.Process.exec_asyncv(command, lambda _, res: callback(AstalIO.Process.exec_asyncv_finish(res)))
|
||||
|
||||
else:
|
||||
AstalIO.Process.exec_async(command, lambda _, res: callback(AstalIO.Process.exec_finish(res)))
|
20
astal/test.hy
Normal file
20
astal/test.hy
Normal file
@ -0,0 +1,20 @@
|
||||
(import astalify [Label Box])
|
||||
(import binding *)
|
||||
(import app [App])
|
||||
|
||||
(import gi)
|
||||
(.require_version gi "Astal" "3.0")
|
||||
|
||||
(import gi.repository [Astal])
|
||||
|
||||
(.start App (dict
|
||||
:main (fn []
|
||||
(setv test (Astal.Window
|
||||
:child (Box
|
||||
:vertical True
|
||||
:children [
|
||||
(Label :label "aoeu")
|
||||
(Label :label "asdf")])))
|
||||
(.show_all test)
|
||||
(.add_window App test))
|
||||
:instance_name "aoeu"))
|
18
astal/time.py
Normal file
18
astal/time.py
Normal file
@ -0,0 +1,18 @@
|
||||
import gi
|
||||
|
||||
from typing import Callable
|
||||
|
||||
gi.require_version("AstalIO", "0.1")
|
||||
gi.require_version("GObject", "2.0")
|
||||
|
||||
from gi.repository import AstalIO, GObject
|
||||
|
||||
def interval(interval: int, callback: Callable) -> AstalIO.Time:
|
||||
return AstalIO.Time.interval(interval, callback)
|
||||
|
||||
def timeout(timeout: int, callback: Callable) -> AstalIO.Time:
|
||||
return AstalIO.Time.timeout(timeout, callback)
|
||||
|
||||
def idle(callback: Callable) -> AstalIO.Time:
|
||||
return AstalIO.Time.idle(callback)
|
||||
|
145
astal/variable.py
Normal file
145
astal/variable.py
Normal file
@ -0,0 +1,145 @@
|
||||
import gi
|
||||
import asyncio
|
||||
from .process import *
|
||||
from .binding import Binding
|
||||
|
||||
gi.require_version("Astal", "3.0")
|
||||
gi.require_version("AstalIO", "0.1")
|
||||
gi.require_version("GObject", "2.0")
|
||||
|
||||
from gi.repository import Astal, AstalIO, GObject
|
||||
|
||||
import threading
|
||||
import time
|
||||
from typing import Any, Callable, List, Optional, Union
|
||||
|
||||
class Variable(AstalIO.VariableBase):
|
||||
def __init__(self, init_value=None):
|
||||
super().__init__()
|
||||
self.value = init_value
|
||||
self.watch_proc = None
|
||||
self.poll_interval = None
|
||||
self.poll_exec = None
|
||||
self.poll_transform = None
|
||||
self.poll_fn = None
|
||||
self.poll_timer = None
|
||||
self.watch_transform = None
|
||||
self.watch_exec = []
|
||||
|
||||
self.connect('dropped', self._on_dropped)
|
||||
|
||||
def __del__(self):
|
||||
self.emit_dropped()
|
||||
|
||||
def subscribe(self, callback):
|
||||
id = self.emitter.connect(
|
||||
'changed',
|
||||
lambda gobject, _=None: callback(self.emitter.get_value())
|
||||
)
|
||||
|
||||
def unsubscribe(_=None):
|
||||
self.emitter.disconnect(id)
|
||||
|
||||
return unsubscribe
|
||||
|
||||
def get_value(self):
|
||||
return self.value
|
||||
|
||||
def set_value(self, new_value):
|
||||
self.value = new_value
|
||||
self.emit_changed()
|
||||
|
||||
def get(self):
|
||||
return self.value
|
||||
|
||||
def set(self, new_value):
|
||||
self.value = new_value
|
||||
self.emit_changed()
|
||||
|
||||
def poll(self, interval, exec, transform=lambda x: x):
|
||||
self.stop_poll()
|
||||
self.poll_transform = transform
|
||||
self.poll_interval = interval
|
||||
if isinstance(exec, Callable):
|
||||
self.poll_fn = exec
|
||||
|
||||
else:
|
||||
self.poll_exec = exec
|
||||
|
||||
self.start_poll()
|
||||
return self
|
||||
|
||||
def start_poll(self):
|
||||
if self.is_polling(): return
|
||||
if not self.poll_transform: return
|
||||
|
||||
if self.poll_fn:
|
||||
self.poll_timer = AstalIO.Time.interval(self.poll_interval, lambda: self.set_value(self.poll_transform(self.poll_fn(self.get_value()))))
|
||||
|
||||
else:
|
||||
self.poll_timer = AstalIO.Time.interval(
|
||||
self.poll_interval,
|
||||
lambda: exec_async(self.poll_exec, lambda out, err=None:
|
||||
self.set_value(self.poll_transform(out, self.get_value()))
|
||||
)
|
||||
)
|
||||
|
||||
def stop_poll(self):
|
||||
if self.is_polling():
|
||||
self.poll_timer.cancel()
|
||||
self.poll_timer = None
|
||||
|
||||
return self
|
||||
|
||||
def watch(self, exec, transform=lambda x, _: x):
|
||||
self.stop_watch()
|
||||
self.watch_transform = transform
|
||||
self.watch_exec = exec
|
||||
self.start_watch()
|
||||
return self
|
||||
|
||||
def start_watch(self):
|
||||
if self.is_watching(): return
|
||||
if not self.watch_transform: return
|
||||
|
||||
self.watch_proc = subprocess(
|
||||
self.watch_exec,
|
||||
lambda _, out: self.set_value(self.watch_transform(out, self.get_value())),
|
||||
)
|
||||
|
||||
def stop_watch(self):
|
||||
if self.is_watching():
|
||||
self.watch_proc.kill()
|
||||
self.watch_proc = None
|
||||
|
||||
return self
|
||||
|
||||
def _on_dropped(self, *_):
|
||||
if self.is_polling():
|
||||
self.stop_poll()
|
||||
if self.is_watching():
|
||||
self.stop_watch()
|
||||
|
||||
def is_polling(self):
|
||||
return self.poll_timer != None
|
||||
|
||||
def is_watching(self):
|
||||
return self.watch_proc != None
|
||||
|
||||
def observe(self, object, signal, callback=lambda _, x: x):
|
||||
# not sure about this
|
||||
object.connect(signal, lambda *args: self.set_value(callback(*args)))
|
||||
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def derive(cls, objects: List[Union[Binding, 'Variable']], transform=lambda *args: args):
|
||||
update = lambda: transform(*map(lambda object: object.get(), objects))
|
||||
|
||||
derived = Variable(update())
|
||||
|
||||
unsubs = [*map(lambda object: object.subscribe(lambda *_: derived.set(update())), objects)]
|
||||
|
||||
derived.connect('dropped', lambda *_: map(lambda unsub: unsub(), unsubs))
|
||||
|
||||
return derived
|
24
main.hy
Normal file
24
main.hy
Normal file
@ -0,0 +1,24 @@
|
||||
(import astal *)
|
||||
(import astal.gtk3 *)
|
||||
|
||||
(import glob [glob])
|
||||
|
||||
(import widgets)
|
||||
|
||||
(defn compile-scss []
|
||||
(exec "sass style/style.scss style/style.css"))
|
||||
|
||||
;; (defn watch-style []
|
||||
;; (lfor file (glob "./style/**" :recursive True)
|
||||
;; (when (not (in ".css" file)) (monitor-file file (fn [_, op] (when (= op 1) (print file op) (compile-scss) (.apply-css App "./style/style.css")))))))
|
||||
|
||||
(compile-scss)
|
||||
|
||||
(.start App
|
||||
:main (fn []
|
||||
;; (.show-all widgets.bar)
|
||||
;; (.add-window App widgets.bar)
|
||||
(.show-all widgets.notifications)
|
||||
(.add-window App widgets.notifications))
|
||||
:instance-name "hy-test"
|
||||
:css "./style/style.css")
|
31
style/colors.scss
Normal file
31
style/colors.scss
Normal file
@ -0,0 +1,31 @@
|
||||
$bg: #161616;
|
||||
$bg-alt-1: #262626;
|
||||
$bg-alt-2: #393939;
|
||||
|
||||
$fg: #f2f4f8;
|
||||
|
||||
$hl: #FF7EB6;
|
||||
$hl-alt-1: #33B1FF;
|
||||
$hl-alt-2: #42be65;
|
||||
|
||||
$icon-color: #f2f4f8;
|
||||
$button-hover-color: #FF7EB6;
|
||||
|
||||
$battery-dial-bg: #42be65;
|
||||
$battery-dial-fg: #161616;
|
||||
|
||||
$volume-dial-bg: #33B1FF;
|
||||
$volume-dial-fg: #161616;
|
||||
|
||||
$brightness-dial-bg: #33B1FF;
|
||||
$brightness-dial-fg: #161616;
|
||||
|
||||
$ws-active: #FF7EB6;
|
||||
$ws-inactive: $bg;
|
||||
|
||||
$mpd-progress-primary: #FF7Eb6;
|
||||
|
||||
$resource-dial-fg-cpu: #FF7eb6;
|
||||
$resource-dial-fg-mem: #FF7eb6;
|
||||
$resource-dial-fg-tmp: #FF7eb6;
|
||||
$resource-dial-fg-fan: #FF7eb6;
|
27
style/mixins.scss
Normal file
27
style/mixins.scss
Normal file
@ -0,0 +1,27 @@
|
||||
@mixin dial($_fg, $_bg, $margin, $padding, $font-size, $icon-font-size) {
|
||||
color: $_fg;
|
||||
background-color: $_bg;
|
||||
margin: $margin;
|
||||
padding: $padding;
|
||||
font-size: $font-size;
|
||||
|
||||
label, icon {
|
||||
font-family: 'Symbols Nerd Font Mono';
|
||||
font-size: $icon-font-size;
|
||||
color: $icon-color;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin bar-dial($_fg, $_bg) {
|
||||
@include dial($_fg, $_bg, 0px, 0px, 5px, 16px)
|
||||
}
|
||||
|
||||
@mixin panel-dial($_fg, $_bg) {
|
||||
@include dial($_fg, $_bg, 0px, 10px, 8px, 32px);
|
||||
margin-left: 10px;
|
||||
border-radius: 10px;
|
||||
.resource-dial {
|
||||
min-width: 96px;
|
||||
min-height: 96px;
|
||||
}
|
||||
}
|
1
style/style.css.map
Normal file
1
style/style.css.map
Normal file
@ -0,0 +1 @@
|
||||
{"version":3,"sourceRoot":"","sources":["style.scss","widgets/bar.scss","colors.scss","mixins.scss","widgets/notifications.scss"],"names":[],"mappings":"AAAA;EACE;;;ACCF;EACE;;;AAKE;EACE,YATG;EAUH,YCVD;EDWC;EACA;;AAEA;EACE;EACA;EACA;EACA,kBCjBG;;ADmBH;EACE,kBCnBC;;ADsBH;EACE,kBCnBL;;ADyBD;EACE;EACA;EACA;EACA;EACA;;AAGE;EACE;;AAIJ;EACE;EACA;EACA;;AAEA;EACE;EACA,YClDC;;ADqDH;EACE;EACA,YC/Ba;;ADqCrB;EACE,YA/DK;EAgEL,YChEC;;ADkED;EACE;;AAEE;EACE;EACA;;AAGF;EACE;EACA;;AAEA;EACE;EACA;EACA,YChFD;;ADmFD;EACE,YC9ED;;ADoFP;EACE;EACA;;AACA;EACE;EACA;EE/FN,ODYgB;ECXhB,kBDYgB;ECXhB,QAY0B;EAX1B,SAW+B;EAV/B,WAUoC;;AARpC;EACE;EACA,WAMuC;EALvC;;AF2FA;EACE,YCrGK;EDsGL;EACA;EACA;;AAGF;EACE;EACA,YC9GD;;ADgHC;EACE;EACA;;;AGhHJ;EACE;EACA;EACA,YFLD;;AEOC;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;;AAGF;EACE,YFpBG;;AEsBH;EACE;EACA,kBFlBC;;AEsBL;EACE;;AAEA;EACE,YFzBC;;AE4BH;EACE,YF/BL","file":"style.css"}
|
9
style/style.scss
Normal file
9
style/style.scss
Normal file
@ -0,0 +1,9 @@
|
||||
* {
|
||||
all: unset;
|
||||
}
|
||||
|
||||
@import 'colors.scss';
|
||||
@import 'mixins.scss';
|
||||
|
||||
@import './widgets/bar.scss';
|
||||
@import './widgets/notifications.scss';
|
119
style/widgets/bar.scss
Normal file
119
style/widgets/bar.scss
Normal file
@ -0,0 +1,119 @@
|
||||
$height: 50px;
|
||||
|
||||
bar {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
centerbox {
|
||||
> box.left {
|
||||
box.workspaces {
|
||||
min-height: $height;
|
||||
background: $bg;
|
||||
padding: 5px;
|
||||
padding-top: 15px;
|
||||
|
||||
box > button {
|
||||
min-width: 10px;
|
||||
min-height: 10px;
|
||||
margin: 5px;
|
||||
background-color: $bg-alt-1;
|
||||
|
||||
&.occupied {
|
||||
background-color: $bg-alt-2;
|
||||
}
|
||||
|
||||
&.focused {
|
||||
background-color: $hl;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
stack > box.player {
|
||||
min-width: 250px;
|
||||
min-height: 60px;
|
||||
background-position: 50% 50%;
|
||||
background-size: cover;
|
||||
border: 4px solid #161616;
|
||||
|
||||
> box {
|
||||
> button {
|
||||
margin: 3px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
> .media-progress {
|
||||
min-width: 242px;
|
||||
min-height: 2px;
|
||||
background: transparent;
|
||||
|
||||
trough {
|
||||
min-height: 2px;
|
||||
background: $bg-alt-1;
|
||||
}
|
||||
|
||||
highlight {
|
||||
min-height: 2px;
|
||||
background: $mpd-progress-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> box.right {
|
||||
min-height: $height;
|
||||
background: $bg;
|
||||
|
||||
box.sliders {
|
||||
margin: 5px;
|
||||
box.volume-slider {
|
||||
> button {
|
||||
font-size: 20px;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
> .volume-slider { /* selecting this as `slider` doesn't work, for some reason.*/
|
||||
min-width: 120px;
|
||||
min-height: 10px;
|
||||
|
||||
& trough {
|
||||
min-height: 10px;
|
||||
min-width: 120px;
|
||||
background: $bg-alt-1;
|
||||
}
|
||||
|
||||
& highlight {
|
||||
background: $hl-alt-1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.battery-container {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
.battery-dial {
|
||||
min-width: 40px;
|
||||
min-height: 40px;
|
||||
@include bar-dial($battery-dial-bg, $battery-dial-fg);
|
||||
}
|
||||
}
|
||||
|
||||
separator {
|
||||
background: $bg-alt-1;
|
||||
padding: 1px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
box.clock {
|
||||
margin: 10px;
|
||||
background: $bg;
|
||||
|
||||
.datetime {
|
||||
margin: 1px;
|
||||
font-size: 10pt;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
44
style/widgets/notifications.scss
Normal file
44
style/widgets/notifications.scss
Normal file
@ -0,0 +1,44 @@
|
||||
.notifications {
|
||||
> * {
|
||||
> button > box {
|
||||
margin: 10px 15px;
|
||||
border: 1px solid $fg;
|
||||
background: $bg;
|
||||
|
||||
.title {
|
||||
font-family: 'tewi';
|
||||
font-size: 22px;
|
||||
min-width: 16rem;
|
||||
}
|
||||
|
||||
.icon {
|
||||
min-width: 64px;
|
||||
min-height: 64px;
|
||||
margin: 5px;
|
||||
font-size: 58px;
|
||||
}
|
||||
|
||||
.timeout-bar {
|
||||
background: $bg-alt-1;
|
||||
|
||||
> trough > progress {
|
||||
background-image: none;
|
||||
background-color: $hl-alt-1;
|
||||
}
|
||||
}
|
||||
|
||||
.urgency-indicator {
|
||||
min-width: 25px;
|
||||
|
||||
&.NORMAL, &.LOW {
|
||||
background: $hl-alt-2;
|
||||
}
|
||||
|
||||
&.CRITICAL {
|
||||
background: $hl;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
2
widgets/__init__.hy
Normal file
2
widgets/__init__.hy
Normal file
@ -0,0 +1,2 @@
|
||||
(import .bar [bar])
|
||||
(import .notifications [notifications])
|
32
widgets/bar/__init__.hy
Normal file
32
widgets/bar/__init__.hy
Normal file
@ -0,0 +1,32 @@
|
||||
(import astal.gtk3 *)
|
||||
(import astal *)
|
||||
|
||||
(import .workspaces [workspaces])
|
||||
(import .mpris [mpris-controls])
|
||||
(import .clock [clock])
|
||||
(import .battery [battery-dial])
|
||||
(import .volume [volume])
|
||||
|
||||
(setv bar (Widget.Window
|
||||
:namespace "bar"
|
||||
:name "bar"
|
||||
:anchor (| Astal.WindowAnchor.TOP Astal.WindowAnchor.LEFT Astal.WindowAnchor.RIGHT)
|
||||
:exclusivity Astal.Exclusivity.EXCLUSIVE
|
||||
:child (Widget.CenterBox
|
||||
:start-widget (Widget.Box
|
||||
:class-name "left"
|
||||
:children [
|
||||
workspaces
|
||||
mpris-controls])
|
||||
:end-widget (Widget.Box
|
||||
:class-name "right"
|
||||
:halign Gtk.Align.END
|
||||
:children [
|
||||
(Widget.Box
|
||||
:class-name "sliders"
|
||||
:vertical True
|
||||
:children [
|
||||
volume])
|
||||
battery-dial
|
||||
((astalify Gtk.Separator))
|
||||
clock]))))
|
27
widgets/bar/battery.hy
Normal file
27
widgets/bar/battery.hy
Normal file
@ -0,0 +1,27 @@
|
||||
(import astal *)
|
||||
(import astal.gtk3 *)
|
||||
|
||||
(.require-version gi "AstalBattery" "0.1")
|
||||
|
||||
(import gi.repository [AstalBattery :as Battery])
|
||||
|
||||
(let [
|
||||
battery (.get-default Battery)
|
||||
icons [
|
||||
["" "" "" "" "" "" "" "" "" "" ""]
|
||||
["" "" "" "" "" "" "" "" "" "" ""]]]
|
||||
(setv battery-dial (Widget.Box
|
||||
:class-name "battery-container"
|
||||
:children [
|
||||
(Widget.CircularProgress
|
||||
:class-name "battery-dial"
|
||||
:rounded False
|
||||
:inverted False
|
||||
:start-at -.25
|
||||
:end-at .75
|
||||
:value (bind battery "percentage")
|
||||
:child (Widget.Label
|
||||
:halign Gtk.Align.CENTER
|
||||
:hexpand True
|
||||
:justify 2
|
||||
:label (.transform (bind battery "percentage") (fn [percentage] (get (get icons (.get-charging battery)) (round (* percentage 10)))))))])))
|
18
widgets/bar/clock.hy
Normal file
18
widgets/bar/clock.hy
Normal file
@ -0,0 +1,18 @@
|
||||
(import astal *)
|
||||
(import astal.gtk3 *)
|
||||
|
||||
(let [
|
||||
datetime (.poll (Variable "") 1000 "date +'%d %b %H:%M:%S'" (fn [out _] out))
|
||||
unix-seconds (.poll (Variable "") 1000 "date +%s" (fn [out _] out))]
|
||||
(setv clock (Widget.Box
|
||||
:class-name "clock"
|
||||
:vertical True
|
||||
:valign Gtk.Align.CENTER
|
||||
:children [
|
||||
(Widget.Label
|
||||
:halign Gtk.Align.START
|
||||
:label (bind datetime))
|
||||
(Widget.Label
|
||||
:halign Gtk.Align.START
|
||||
:label (bind unix-seconds))])))
|
||||
|
81
widgets/bar/mpris.hy
Normal file
81
widgets/bar/mpris.hy
Normal file
@ -0,0 +1,81 @@
|
||||
(import astal *)
|
||||
(import astal.gtk3 *)
|
||||
|
||||
(import math)
|
||||
|
||||
(.require-version gi "AstalMpris" "0.1")
|
||||
|
||||
(import gi.repository [AstalMpris :as Mpris])
|
||||
|
||||
(let [mpris (.get-default Mpris)]
|
||||
(setv mpris-controls
|
||||
(Widget.Stack
|
||||
:transition-type Gtk.StackTransitionType.SLIDE_UP_DOWN
|
||||
:transition-duration 125
|
||||
:children []
|
||||
:setup (fn [self]
|
||||
(.add-events self Gdk.EventMask.SCROLL_MASK)
|
||||
(.add-events self Gdk.EventMask.SMOOTH_SCROLL_MASK)
|
||||
|
||||
(defn add-player [player]
|
||||
(.add-named self
|
||||
(Widget.Box
|
||||
:class-name "player"
|
||||
:vertical True
|
||||
:hexpand False
|
||||
:setup (fn [self] (.set-name self (.get-bus-name player)))
|
||||
:css (.transform (bind player "cover-art") (fn [cover-uri]
|
||||
(when cover-uri (return f"
|
||||
background: linear-gradient(to right, rgba(0, 0, 0, 0), rgba(22, 22, 22, 0.925)), url(\"{cover-uri}\");
|
||||
background-position: 50% 50%;
|
||||
background-size: cover;
|
||||
"))
|
||||
(return "")))
|
||||
:children [
|
||||
(Widget.Box
|
||||
:vertical True
|
||||
:vexpand True
|
||||
:valign Gtk.Align.CENTER
|
||||
:halign Gtk.Align.END
|
||||
:children [
|
||||
(Widget.Button
|
||||
:on-clicked (fn [#* _] (.previous player))
|
||||
:child (Widget.Icon :icon "media-skip-backward-symbolic"))
|
||||
(Widget.Button
|
||||
:on-clicked (fn [#* _] (.play-pause player))
|
||||
:child (Widget.Icon :icon (.transform (bind player "playback-status") (fn [status]
|
||||
(when (= status Mpris.PlaybackStatus.PLAYING)
|
||||
(return "media-playback-pause-symbolic"))
|
||||
(return "media-playback-start-symbolic")))))
|
||||
(Widget.Button
|
||||
:on-clicked (fn [#* _] (.next player))
|
||||
:child (Widget.Icon :icon "media-skip-forward-symbolic"))])
|
||||
(Widget.Slider
|
||||
:hexpand True
|
||||
:class-name "media-progress"
|
||||
:valign Gtk.Align.END
|
||||
:halign Gtk.Align.START
|
||||
:on-dragged (fn [self] (.set-position player (* (.get-value self) (.get-length player))))
|
||||
:setup (fn [self]
|
||||
(.poll (Variable) 500 (fn [#* _]
|
||||
(when player
|
||||
(.set-value self (/ (.get-position player) (.get-length player))))))))])
|
||||
(.get-bus-name player)))
|
||||
|
||||
(for [player (.get-players mpris)] (add-player player))
|
||||
|
||||
(.hook self mpris "player-added" (fn [self player]
|
||||
(add-player player)))
|
||||
|
||||
(.hook self mpris "player-closed" (fn [self player]
|
||||
(.destroy (.get-named self (.get-bus-name player)))))
|
||||
|
||||
(setv accumulated-delta-y 0)
|
||||
|
||||
(.hook self self "scroll-event" (fn [_ event]
|
||||
(nonlocal accumulated-delta-y)
|
||||
(setv accumulated-delta-y (+ accumulated-delta-y (get (.get-scroll-deltas event) 2)))
|
||||
|
||||
(when (> (abs accumulated-delta-y) 10)
|
||||
(.set-visible-child self (get (.get-children self) (% (+ (.index (lfor child (.get-children self) (.get-name child)) (.get-shown self)) (* 1 (round (/ accumulated-delta-y 10)))) (len (.get-children self)))))
|
||||
(setv accumulated-delta-y 0))))))))
|
22
widgets/bar/volume.hy
Normal file
22
widgets/bar/volume.hy
Normal file
@ -0,0 +1,22 @@
|
||||
(import astal *)
|
||||
(import astal.gtk3 *)
|
||||
|
||||
(.require-version gi "AstalWp" "0.1")
|
||||
|
||||
(import gi.repository [AstalWp])
|
||||
|
||||
(let [
|
||||
endpoint (. AstalWp (get-default) (get-audio) (get-default-speaker))]
|
||||
(setv volume (Widget.Box
|
||||
:class-name "volume-slider"
|
||||
:children [
|
||||
(Widget.Button
|
||||
:child (Widget.Icon :icon (bind endpoint "volume-icon"))
|
||||
:on-clicked (fn [#* _] (.set-mute endpoint (not (.get-mute endpoint)))))
|
||||
(Widget.Slider
|
||||
:class-name "volume-slider"
|
||||
:hexpand True
|
||||
:draw-value False
|
||||
:value (bind endpoint "volume")
|
||||
:on-dragged (fn [self]
|
||||
(.set-volume endpoint (.get-value self))))])))
|
41
widgets/bar/workspaces.hy
Normal file
41
widgets/bar/workspaces.hy
Normal file
@ -0,0 +1,41 @@
|
||||
(import astal *)
|
||||
(import astal.gtk3 *)
|
||||
|
||||
(.require-version gi "AstalHyprland" "0.1")
|
||||
|
||||
(import gi.repository [AstalHyprland :as Hyprland])
|
||||
|
||||
|
||||
(let [
|
||||
hyprland (.get-default Hyprland)
|
||||
workspace-row (fn [start stop]
|
||||
(Widget.Box
|
||||
:children (lfor i (range start stop)
|
||||
(Widget.Button
|
||||
:class-name "workspace"
|
||||
:attribute (+ i 1)
|
||||
:on-clicked (fn [self] (.message-async hyprland f"dispatch workspace {self.attribute}"))
|
||||
:setup (fn [self]
|
||||
(.hook self hyprland "notify::focused-workspace" (fn [self, w] (.toggle-class-name self "focused" (= (.get-id w) self.attribute))))
|
||||
(defn update [#* _]
|
||||
(let [workspace (.get-workspace hyprland self.attribute)]
|
||||
(when (!= workspace None)
|
||||
(.toggle-class-name self "occupied" (< 0 (len (.get-clients workspace)))))))
|
||||
|
||||
(.hook self hyprland "notify::workspaces" update)
|
||||
(.hook self hyprland "notify::clients" update)
|
||||
(.hook self hyprland "client-moved" update)
|
||||
(update)
|
||||
|
||||
(when (= (.get-id (.get-focused-workspace hyprland)) self.attribute)
|
||||
(.toggle-class-name self "focused")))))))]
|
||||
|
||||
(setv workspaces (Widget.Box
|
||||
:class_name "workspaces"
|
||||
:vertical True
|
||||
:hexpand False
|
||||
:halign Gtk.Align.START
|
||||
:valign Gtk.Align.CENTER
|
||||
:children [
|
||||
(workspace-row 0 5)
|
||||
(workspace-row 5 10)])))
|
107
widgets/notifications/__init__.hy
Normal file
107
widgets/notifications/__init__.hy
Normal file
@ -0,0 +1,107 @@
|
||||
(import astal *)
|
||||
(import astal.gtk3 *)
|
||||
|
||||
(.require-version gi "AstalNotifd" "0.1")
|
||||
(.require-version gi "Pango" "1.0")
|
||||
|
||||
(import gi.repository [AstalNotifd Pango])
|
||||
|
||||
(let [
|
||||
ProgressBar (astalify Gtk.ProgressBar)
|
||||
notifd (.get-default AstalNotifd)
|
||||
notification-timeout 3
|
||||
notification-icon (fn [notification]
|
||||
(cond
|
||||
(.get-image notification) (Widget.Box
|
||||
:class-name "icon image"
|
||||
:css f"
|
||||
background-image: url(\"{(.get-image notification)}\");
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;")
|
||||
(. Astal Icon (lookup-icon (.get-app-icon notification))) (Widget.Icon
|
||||
:icon (.get-app-icon notification)
|
||||
:class-name "icon")
|
||||
True (Widget.Icon
|
||||
:icon "dialog-information-symbolic"
|
||||
:class-name "icon")))
|
||||
|
||||
make-notification (fn [notification]
|
||||
(let [
|
||||
layout (Widget.Button
|
||||
:on-clicked (fn [self] (. self (get-parent) (set-reveal-child False)))
|
||||
:child (Widget.Box
|
||||
:children [
|
||||
(Widget.Box
|
||||
:vertical True
|
||||
:css "min-width: 200px; min-height 50px;;"
|
||||
:children [
|
||||
(Widget.Box
|
||||
:children [
|
||||
(notification-icon notification)
|
||||
(Widget.Box
|
||||
:vertical True
|
||||
:children [
|
||||
(Widget.Label
|
||||
:class-name "title"
|
||||
:label (str (.get-summary notification))
|
||||
:xalign 0
|
||||
:justify 0
|
||||
:ellipsize Pango.EllipsizeMode.END
|
||||
)
|
||||
(Widget.Label
|
||||
:class-name "body"
|
||||
:label (str (.get-body notification))
|
||||
:xalign 0
|
||||
:justify 0
|
||||
:wrap True
|
||||
:wrap-mode Pango.WrapMode.WORD_CHAR
|
||||
:use-markup True
|
||||
)])])
|
||||
(ProgressBar
|
||||
:class-name "timeout-bar"
|
||||
:hexpand True
|
||||
:valign 2
|
||||
:fraction (bind (.poll (Variable 1) (// (+ (* (or (max (.get-expire-timeout notification) 0) notification-timeout) 1000) 250) 100) (fn [prev]
|
||||
(when (> prev .02)
|
||||
(return (- prev .01)))
|
||||
(return 0)))))])
|
||||
(Widget.Box :class-name f"urgency-indicator {(.get-urgency notification)}")]))]
|
||||
|
||||
(Widget.Revealer
|
||||
:transition-type Gtk.RevealerTransitionType.SLIDE_DOWN
|
||||
:transition-duration 250
|
||||
:class-name "notifications"
|
||||
:child (Widget.Revealer
|
||||
:transition-type Gtk.RevealerTransitionType.SLIDE_DOWN
|
||||
:transition-duration 250
|
||||
:child layout
|
||||
:setup (fn [self]
|
||||
(.hook self self "notify::reveal-child" (fn [#* _]
|
||||
(when (not (.get-reveal-child self))
|
||||
(timeout 250 (fn [] (. self (get-parent) (set-reveal-child False)))))))))
|
||||
:setup (fn [self]
|
||||
(.hook self self "notify::reveal-child" (fn [self revealed?]
|
||||
(when revealed?
|
||||
(when (.get-reveal-child self) (timeout 1 (fn [] (. self (get-child) (set-reveal-child True)))))
|
||||
(timeout (* 1000 (or (max (.get-expire-timeout notification) 0) notification-timeout)) (fn []
|
||||
(. self (get-child) (set-reveal-child False))
|
||||
(timeout (. self (get-child) (get-transition-duration)) (fn []
|
||||
(.set-reveal-child self False)
|
||||
(timeout (.get-transition-duration self) (fn []
|
||||
(.destroy self))))))))))
|
||||
|
||||
(.set-reveal-child self True)))))]
|
||||
|
||||
(setv notifications (Widget.Window
|
||||
:namespace "notifications"
|
||||
:name "notifications"
|
||||
:anchor (| Astal.WindowAnchor.TOP Astal.WindowAnchor.RIGHT)
|
||||
:exclusivity Astal.Exclusivity.EXCLUSIVE
|
||||
:margin-top 5
|
||||
:child (Widget.Box
|
||||
:vertical True
|
||||
:setup (fn [self]
|
||||
(.hook self notifd "notified" (fn [self notification _]
|
||||
(let [children (.get-children self)]
|
||||
(.add self (make-notification (.get-notification notifd notification)))))))))))
|
Loading…
x
Reference in New Issue
Block a user