🔧 - This commit adds example configurations for several features and widgets, including app launcher, status bar, bluetooth widget, desktop widget, icon browser, notifications, and side panel.

This commit is contained in:
sankalp committed 2025-07-04 12:41:17 +05:45
1 parent bc11606e11
commit aeebf26b8d
14 files changed
+1596 -12

No files matched your search

+149
View File
@@ -0,0 +1,149 @@
"""
example configuration shows how to make a simple
desktop applications launcher, this example doesn't involve
any styling (except a couple of basic style properties)
the purpose of this configuration is to show to to use
the given utils and mainly how using lazy executors might
make the configuration way more faster than it's supposed to be
"""
import operator
from collections.abc import Iterator
from fabric import Application
from fabric.widgets.box import Box
from fabric.widgets.label import Label
from fabric.widgets.button import Button
from fabric.widgets.image import Image
from fabric.widgets.entry import Entry
from fabric.widgets.scrolledwindow import ScrolledWindow
from fabric.widgets.wayland import WaylandWindow as Window
from fabric.utils import DesktopApp, get_desktop_applications, idle_add, remove_handler
class AppLauncher(Window):
def __init__(self, **kwargs):
super().__init__(
layer="top",
anchor="center",
exclusivity="none",
keyboard_mode="on-demand",
visible=False,
all_visible=False,
**kwargs,
)
self._arranger_handler: int = 0
self._all_apps = get_desktop_applications()
self.viewport = Box(spacing=2, orientation="v")
self.search_entry = Entry(
placeholder="Search Applications...",
h_expand=True,
notify_text=lambda entry, *_: self.arrange_viewport(entry.get_text()),
)
self.scrolled_window = ScrolledWindow(
min_content_size=(280, 320),
max_content_size=(280 * 2, 320),
child=self.viewport,
)
self.add(
Box(
spacing=2,
orientation="v",
style="margin: 2px",
children=[
# the header with the search entry
Box(
spacing=2,
orientation="h",
children=[
self.search_entry,
Button(
image=Image(icon_name="window-close"),
tooltip_text="Exit",
on_clicked=lambda *_: self.application.quit(),
),
],
),
# the actual slots holder
self.scrolled_window,
],
)
)
self.show_all()
def arrange_viewport(self, query: str = ""):
# reset everything so we can filter current viewport's slots...
# remove the old handler so we can avoid race conditions
remove_handler(self._arranger_handler) if self._arranger_handler else None
# remove all children from the viewport
self.viewport.children = []
# make a new iterator containing the filtered apps
filtered_apps_iter = iter(
[
app
for app in self._all_apps
if query.casefold()
in (
(app.display_name or "")
+ (" " + app.name + " ")
+ (app.generic_name or "")
).casefold()
]
)
should_resize = operator.length_hint(filtered_apps_iter) == len(self._all_apps)
# all aboard...
# start the process of adding slots with a lazy executor
# using this method makes the process of adding slots way more less
# resource expensive without blocking the main thread and resulting in a lock
self._arranger_handler = idle_add(
lambda *args: self.add_next_application(*args)
or (self.resize_viewport() if should_resize else False),
filtered_apps_iter,
pin=True,
)
return False
def add_next_application(self, apps_iter: Iterator[DesktopApp]):
if not (app := next(apps_iter, None)):
return False
self.viewport.add(self.bake_application_slot(app))
return True
def resize_viewport(self):
self.scrolled_window.set_min_content_width(
self.viewport.get_allocation().width # type: ignore
)
return False
def bake_application_slot(self, app: DesktopApp, **kwargs) -> Button:
return Button(
child=Box(
orientation="h",
spacing=12,
children=[
Image(pixbuf=app.get_icon_pixbuf(), h_align="start", size=32),
Label(
label=app.display_name or "Unknown",
v_align="center",
h_align="center",
),
],
),
tooltip_text=app.description,
on_clicked=lambda *_: (app.launch(), self.application.quit()),
**kwargs,
)
if __name__ == "__main__":
app_launcher = AppLauncher()
app = Application("app-launcher", app_launcher)
app.run()
+170
View File
@@ -0,0 +1,170 @@
import psutil
from fabric import Application
from fabric.widgets.box import Box
from fabric.widgets.label import Label
from fabric.widgets.overlay import Overlay
from fabric.widgets.eventbox import EventBox
from fabric.widgets.datetime import DateTime
from fabric.widgets.centerbox import CenterBox
from fabric.system_tray.widgets import SystemTray
from fabric.widgets.circularprogressbar import CircularProgressBar
from wayland import WaylandWindow as Window
from fabric.hyprland.widgets import Language, ActiveWindow, Workspaces, WorkspaceButton
from fabric.utils import (
FormattedString,
bulk_replace,
invoke_repeater,
get_relative_path,
)
AUDIO_WIDGET = True
if AUDIO_WIDGET is True:
try:
from fabric.audio.service import Audio
except Exception as e:
print(e)
AUDIO_WIDGET = False
class VolumeWidget(Box):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.audio = Audio()
self.progress_bar = CircularProgressBar(
name="volume-progress-bar", pie=True, size=24
)
self.event_box = EventBox(
events="scroll",
child=Overlay(
child=self.progress_bar,
overlays=Label(
label="",
style="margin: 0px 6px 0px 0px; font-size: 12px", # to center the icon glyph
),
),
)
self.audio.connect("notify::speaker", self.on_speaker_changed)
self.event_box.connect("scroll-event", self.on_scroll)
self.add(self.event_box)
def on_scroll(self, _, event):
match event.direction:
case 0:
self.audio.speaker.volume += 8
case 1:
self.audio.speaker.volume -= 8
return
def on_speaker_changed(self, *_):
if not self.audio.speaker:
return
self.progress_bar.value = self.audio.speaker.volume / 100
self.audio.speaker.bind(
"volume", "value", self.progress_bar, lambda _, v: v / 100
)
return
class StatusBar(Window):
def __init__(
self,
):
super().__init__(
name="bar",
layer="top",
anchor="left top right",
margin="10px 10px -2px 10px",
exclusivity="auto",
visible=False,
all_visible=False,
)
self.workspaces = Workspaces(
name="workspaces",
spacing=4,
buttons_factory=lambda ws_id: WorkspaceButton(id=ws_id, label=None),
)
self.active_window = ActiveWindow(name="hyprland-window")
self.language = Language(
formatter=FormattedString(
"{replace_lang(language)}",
replace_lang=lambda lang: bulk_replace(
lang,
(r".*Eng.*", r".*Ar.*"),
("ENG", "ARA"),
regex=True,
),
),
name="hyprland-window",
)
self.date_time = DateTime(name="date-time")
self.system_tray = SystemTray(name="system-tray", spacing=4)
self.ram_progress_bar = CircularProgressBar(
name="ram-progress-bar", pie=True, size=24
)
self.cpu_progress_bar = CircularProgressBar(
name="cpu-progress-bar", pie=True, size=24
)
self.progress_bars_overlay = Overlay(
child=self.ram_progress_bar,
overlays=[
self.cpu_progress_bar,
Label("", style="margin: 0px 6px 0px 0px; font-size: 12px"),
],
)
self.status_container = Box(
name="widgets-container",
spacing=4,
orientation="h",
children=self.progress_bars_overlay,
)
self.status_container.add(VolumeWidget()) if AUDIO_WIDGET is True else None
self.children = CenterBox(
name="bar-inner",
start_children=Box(
name="start-container",
spacing=4,
orientation="h",
children=self.workspaces,
),
center_children=Box(
name="center-container",
spacing=4,
orientation="h",
children=self.active_window,
),
end_children=Box(
name="end-container",
spacing=4,
orientation="h",
children=[
self.status_container,
self.system_tray,
self.date_time,
self.language,
],
),
)
invoke_repeater(1000, self.update_progress_bars)
self.show_all()
def update_progress_bars(self):
self.ram_progress_bar.value = psutil.virtual_memory().percent / 100
self.cpu_progress_bar.value = psutil.cpu_percent() / 100
return True
if __name__ == "__main__":
bar = StatusBar()
app = Application("bar", bar)
app.set_stylesheet_from_file(get_relative_path("./style.css"))
app.run()
+169
View File
@@ -0,0 +1,169 @@
/* we can use webcss variables, fabric compiles that to gtk css.
global variables can be stored in :vars */
:vars {
--background: #150d16;
--foreground: #f5dbc4;
--color0: #150d16;
--color1: #72448D;
--color2: #9C5995;
--color3: #D5719F;
--color4: #9A62F3;
--color5: #E075DF;
--color6: #F98F9F;
--color7: #f5dbc4;
--color8: #ab9989;
--color9: #72448D;
--color10: #9C5995;
--color11: #D5719F;
--color12: #9A62F3;
--color13: #E075DF;
--color14: #F98F9F;
--color15: #f5dbc4;
--window-bg: alpha(var(--background), 0.6);
--module-bg: alpha(var(--background), 0.4);
--border-color: var(--color11);
--ws-active: var(--color9);
--ws-inactive: var(--color3);
--ws-empty: var(--color8);
--ws-hover: var(--color1);
--ws-urgent: var(--color12);
}
/* unset so we can style everything from the ground up. */
* {
all: unset;
color: var(--foreground);
font-size: 16px;
font-family: "Jost*", sans-serif;
border-radius: 100px;
}
button {
background-size: 400% 400%;
}
#bar-inner {
padding: 4px;
border: solid 2px;
border-color: var(--border-color);
background-color: var(--window-bg);
min-height: 28px;
}
#workspaces {
padding: 6px;
min-width: 0px;
background-color: var(--module-bg);
}
#workspaces>button {
padding: 0px 8px;
transition: padding 0.05s steps(8);
background-color: var(--ws-inactive);
}
#workspaces>button>label {
font-size: 0px;
}
#workspaces>button:hover {
background-color: var(--ws-hover);
}
#workspaces>button.urgent {
background-color: var(--ws-urgent);
}
#workspaces>button.active {
padding: 0px 32px;
background-color: var(--ws-active);
}
#workspaces>button.empty {
background-color: var(--ws-empty);
}
#date-time,
#hyprland-language,
#hyprland-window {
background-color: var(--module-bg);
padding: 0px 8px;
}
menu>menuitem>label,
#date-time>label,
#hyprland-language>label,
#hyprland-window>label {
font-weight: 900;
}
/* system tray */
#system-tray {
padding: 2px 4px;
background-color: var(--module-bg);
}
/* menu and menu items (written for the system tray) */
menu {
border: solid 2px;
border-radius: 10px;
border-color: var(--border-color);
background-color: var(--window-bg);
}
menu>menuitem {
border-radius: 0px;
background-color: var(--module-bg);
padding: 6px;
margin-left: 2px;
margin-right: 2px;
}
menu>menuitem:first-child {
margin-top: 1px;
border-radius: 8px 8px 0px 0px;
}
menu>menuitem:last-child {
margin-bottom: 1px;
border-radius: 0px 0px 8px 8px;
}
menu>menuitem:hover {
background-color: var(--border-color);
}
#cpu-progress-bar,
#ram-progress-bar,
#volume-progress-bar {
color: transparent;
background-color: transparent
}
#cpu-progress-bar {
border: solid 0px alpha(var(--color9), 0.8);
}
#ram-progress-bar,
#volume-progress-bar {
border: solid 0px var(--border-color);
}
#widgets-container {
background-color: var(--module-bg);
padding: 2px;
}
tooltip {
border: solid 2px;
border-color: var(--border-color);
background-color: var(--window-bg);
}
tooltip>* {
padding: 2px 4px
}
+359
View File
@@ -0,0 +1,359 @@
from gi.repository import Gdk, GObject, Gtk
import re
from collections.abc import Iterable
from enum import Enum
from typing import Literal, cast
import cairo
import gi
from fabric.core.service import Property
from fabric.utils.helpers import extract_css_values, get_enum_member
from fabric.widgets.window import Window
from loguru import logger
gi.require_version("Gtk", "3.0")
try:
gi.require_version("GtkLayerShell", "0.1")
from gi.repository import GtkLayerShell
except:
raise ImportError(
"looks like we don't have gtk-layer-shell installed, make sure to install it first (as well as using wayland)"
)
class WaylandWindowExclusivity(Enum):
NONE = 1
NORMAL = 2
AUTO = 3
class Layer(GObject.GEnum):
BACKGROUND = 0
BOTTOM = 1
TOP = 2
OVERLAY = 3
ENTRY_NUMBER = 4
class KeyboardMode(GObject.GEnum):
NONE = 0
EXCLUSIVE = 1
ON_DEMAND = 2
ENTRY_NUMBER = 3
class Edge(GObject.GEnum):
LEFT = 0
RIGHT = 1
TOP = 2
BOTTOM = 3
ENTRY_NUMBER = 4
class WaylandWindow(Window):
@Property(
Layer,
flags="read-write",
default_value=Layer.TOP,
)
def layer(self) -> Layer: # type: ignore
return self._layer # type: ignore
@layer.setter
def layer(
self,
value: Literal["background", "bottom", "top", "overlay"] | Layer,
) -> None:
self._layer = get_enum_member(Layer, value, default=Layer.TOP)
return GtkLayerShell.set_layer(self, self._layer)
@Property(int, "read-write")
def monitor(self) -> int:
if not (monitor := cast(Gdk.Monitor, GtkLayerShell.get_monitor(self))):
return -1
display = monitor.get_display() or Gdk.Display.get_default()
for i in range(0, display.get_n_monitors()):
if display.get_monitor(i) is monitor:
return i
return -1
@monitor.setter
def monitor(self, monitor: int | Gdk.Monitor) -> bool:
if isinstance(monitor, int):
display = Gdk.Display().get_default()
monitor = display.get_monitor(monitor)
return (
(GtkLayerShell.set_monitor(self, monitor), True)[1]
if monitor is not None
else False
)
@Property(WaylandWindowExclusivity, "read-write")
def exclusivity(self) -> WaylandWindowExclusivity:
return self._exclusivity
@exclusivity.setter
def exclusivity(
self, value: Literal["none", "normal", "auto"] | WaylandWindowExclusivity
) -> None:
value = get_enum_member(
WaylandWindowExclusivity, value, default=WaylandWindowExclusivity.NONE
)
self._exclusivity = value
match value:
case WaylandWindowExclusivity.NORMAL:
return GtkLayerShell.set_exclusive_zone(self, True)
case WaylandWindowExclusivity.AUTO:
return GtkLayerShell.auto_exclusive_zone_enable(self)
case _:
return GtkLayerShell.set_exclusive_zone(self, False)
@Property(bool, "read-write", default_value=False)
def pass_through(self) -> bool:
return self._pass_through
@pass_through.setter
def pass_through(self, pass_through: bool = False):
self._pass_through = pass_through
region = cairo.Region() if pass_through is True else None
self.input_shape_combine_region(region)
del region
return
@Property(
KeyboardMode,
"read-write",
default_value=KeyboardMode.NONE,
)
def keyboard_mode(self) -> KeyboardMode:
return self._keyboard_mode
@keyboard_mode.setter
def keyboard_mode(
self,
value: Literal[
"none",
"exclusive",
"on-demand",
"entry-number",
]
| KeyboardMode,
):
self._keyboard_mode = get_enum_member(
KeyboardMode, value, default=KeyboardMode.NONE
)
return GtkLayerShell.set_keyboard_mode(self, self._keyboard_mode)
@Property(tuple[Edge, ...], "read-write")
def anchor(self):
return tuple(
x
for x in [
Edge.TOP,
Edge.RIGHT,
Edge.BOTTOM,
Edge.LEFT,
]
if GtkLayerShell.get_anchor(self, x)
)
@anchor.setter
def anchor(self, value: str | Iterable[Edge]) -> None:
self._anchor = value
if isinstance(value, (list, tuple)) and all(
isinstance(edge, Edge) for edge in value
):
for edge in [
Edge.TOP,
Edge.RIGHT,
Edge.BOTTOM,
Edge.LEFT,
]:
if edge not in value:
GtkLayerShell.set_anchor(self, edge, False)
GtkLayerShell.set_anchor(self, edge, True)
return
elif isinstance(value, str):
for edge, anchored in WaylandWindow.extract_edges_from_string(
value
).items():
GtkLayerShell.set_anchor(self, edge, anchored)
return
@Property(tuple[int, ...], flags="read-write")
def margin(self) -> tuple[int, ...]:
return tuple(
GtkLayerShell.get_margin(self, x)
for x in [
Edge.TOP,
Edge.RIGHT,
Edge.BOTTOM,
Edge.LEFT,
]
)
@margin.setter
def margin(self, value: str | Iterable[int]) -> None:
for edge, mrgv in WaylandWindow.extract_margin(value).items():
GtkLayerShell.set_margin(self, edge, mrgv)
return
@Property(object, "read-write")
def keyboard_mode(self):
kb_mode = GtkLayerShell.get_keyboard_mode(self)
if GtkLayerShell.get_keyboard_interactivity(self):
kb_mode = KeyboardMode.EXCLUSIVE
return kb_mode
@keyboard_mode.setter
def keyboard_mode(
self,
value: Literal["none", "exclusive", "on-demand"] | KeyboardMode,
):
return GtkLayerShell.set_keyboard_mode(
self,
get_enum_member(
KeyboardMode,
value,
default=KeyboardMode.NONE,
),
)
def __init__(
self,
layer: Literal["background", "bottom", "top", "overlay"] | Layer = Layer.TOP,
anchor: str = "",
margin: str | Iterable[int] = "0px 0px 0px 0px",
exclusivity: Literal["auto", "normal", "none"]
| WaylandWindowExclusivity = WaylandWindowExclusivity.NONE,
keyboard_mode: Literal["none", "exclusive", "on-demand"]
| KeyboardMode = KeyboardMode.NONE,
pass_through: bool = False,
monitor: int | Gdk.Monitor | None = None,
title: str = "fabric",
type: Literal["top-level", "popup"] | Gtk.WindowType = Gtk.WindowType.TOPLEVEL,
child: Gtk.Widget | None = None,
name: str | None = None,
visible: bool = True,
all_visible: bool = False,
style: str | None = None,
style_classes: Iterable[str] | str | None = None,
tooltip_text: str | None = None,
tooltip_markup: str | None = None,
h_align: Literal["fill", "start", "end", "center", "baseline"]
| Gtk.Align
| None = None,
v_align: Literal["fill", "start", "end", "center", "baseline"]
| Gtk.Align
| None = None,
h_expand: bool = False,
v_expand: bool = False,
size: Iterable[int] | int | None = None,
**kwargs,
):
Window.__init__(
self,
title=title,
type=type,
child=child,
name=name,
visible=False,
all_visible=False,
style=style,
style_classes=style_classes,
tooltip_text=tooltip_text,
tooltip_markup=tooltip_markup,
h_align=h_align,
v_align=v_align,
h_expand=h_expand,
v_expand=v_expand,
size=size,
**kwargs,
)
self._layer = Layer.ENTRY_NUMBER
self._keyboard_mode = KeyboardMode.NONE
self._anchor = anchor
self._exclusivity = WaylandWindowExclusivity.NONE
self._pass_through = pass_through
GtkLayerShell.init_for_window(self)
GtkLayerShell.set_namespace(self, title)
self.connect(
"notify::title",
lambda *_: GtkLayerShell.set_namespace(self, self.get_title()),
)
if monitor is not None:
self.monitor = monitor
self.layer = layer
self.anchor = anchor
self.margin = margin
self.keyboard_mode = keyboard_mode
self.exclusivity = exclusivity
self.pass_through = pass_through
self.show_all() if all_visible is True else self.show() if visible is True else None
def steal_input(self) -> None:
return GtkLayerShell.set_keyboard_interactivity(self, True)
def return_input(self) -> None:
return GtkLayerShell.set_keyboard_interactivity(self, False)
# custom overrides
def show(self) -> None:
super().show()
return self.do_handle_post_show_request()
def show_all(self) -> None:
super().show_all()
return self.do_handle_post_show_request()
def do_handle_post_show_request(self) -> None:
if not self.get_children():
logger.warning(
"[WaylandWindow] showing an empty window is not recommended, some compositors might freak out."
)
self.pass_through = self._pass_through
return
@staticmethod
def extract_anchor_values(string: str) -> tuple[str, ...]:
"""
extracts the geometry values from a given geometry string.
:param string: the string containing the geometry values.
:type string: str
:return: a list of unique directions extracted from the geometry string.
:rtype: list
"""
direction_map = {"l": "left", "t": "top", "r": "right", "b": "bottom"}
pattern = re.compile(r"\b(left|right|top|bottom)\b", re.IGNORECASE)
matches = pattern.findall(string)
return tuple(set(tuple(direction_map[match.lower()[0]] for match in matches)))
@staticmethod
def extract_edges_from_string(string: str) -> dict["Edge", bool]:
anchor_values = WaylandWindow.extract_anchor_values(string.lower())
return {
Edge.TOP: "top" in anchor_values,
Edge.RIGHT: "right" in anchor_values,
Edge.BOTTOM: "bottom" in anchor_values,
Edge.LEFT: "left" in anchor_values,
}
@staticmethod
def extract_margin(input: str | Iterable[int]) -> dict["Edge", int]:
margins = (
extract_css_values(input.lower())
if isinstance(input, str)
else input
if isinstance(input, (tuple, list)) and len(input) == 4
else (0, 0, 0, 0)
)
return {
Edge.TOP: margins[0],
Edge.RIGHT: margins[1],
Edge.BOTTOM: margins[2],
Edge.LEFT: margins[3],
}
+112
View File
@@ -0,0 +1,112 @@
from fabric import Application
from fabric.widgets.box import Box
from fabric.widgets.label import Label
from fabric.widgets.image import Image
from fabric.widgets.button import Button
from fabric.widgets.window import Window
from fabric.widgets.centerbox import CenterBox
from fabric.widgets.scrolledwindow import ScrolledWindow
from fabric.bluetooth import BluetoothClient, BluetoothDevice
class BluetoothDeviceSlot(CenterBox):
def __init__(self, device: BluetoothDevice, **kwargs):
super().__init__(**kwargs)
self.device = device
self.device.connect("changed", self.on_changed)
self.device.connect(
"notify::closed", lambda *_: self.device.closed and self.destroy()
)
self.connection_label = Label(label="Disconnected")
self.connect_button = Button(
label="Connect",
on_clicked=lambda *_: self.device.set_connecting(not self.device.connected),
)
self.start_children = [
Image(icon_name=device.icon_name + "-symbolic", size=32),
Label(label=device.name),
]
self.center_children = self.connection_label
self.end_children = self.connect_button
self.device.emit("changed") # to update display status
def on_changed(self, *_):
self.connection_label.set_label(
"Connected" if self.device.connected else "Disconnected"
)
if self.device.connecting:
self.connect_button.set_label(
"Connecting..." if not self.device.connecting else "Disconnecting..."
)
else:
self.connect_button.set_label(
"Connect" if not self.device.connected else "Disconnect"
)
return
class BluetoothConnections(Box):
def __init__(self, **kwargs):
super().__init__(
spacing=4,
orientation="vertical",
style="margin: 8px",
**kwargs,
)
self.client = BluetoothClient(on_device_added=self.on_device_added)
self.scan_button = Button(on_clicked=lambda *_: self.client.toggle_scan())
self.toggle_button = Button(on_clicked=lambda *_: self.client.toggle_power())
self.client.connect(
"notify::enabled",
lambda *_: self.toggle_button.set_label(
"Bluetooth " + ("ON" if self.client.enabled else "OFF")
),
)
self.client.connect(
"notify::scanning",
lambda *_: self.scan_button.set_label(
"Stop" if self.client.scanning else "Scan"
),
)
self.paired_box = Box(spacing=2, orientation="vertical")
self.available_box = Box(spacing=2, orientation="vertical")
self.children = [
CenterBox(start_children=self.scan_button, end_children=self.toggle_button),
Label("Paired Devices"),
ScrolledWindow(min_content_size=(400, -1), child=self.paired_box),
Label("Available Devices"),
ScrolledWindow(min_content_size=(-1, 400), child=self.available_box),
]
# to run notify closures thus display the status
# without having to wait until an actual change
self.client.notify("scanning")
self.client.notify("enabled")
def on_device_added(self, client: BluetoothClient, address: str):
if not (device := client.get_device(address)):
return
slot = BluetoothDeviceSlot(device)
if device.paired:
return self.paired_box.add(slot)
return self.available_box.add(slot)
if __name__ == "__main__":
bluetooth_widget = Window(
child=BluetoothConnections(),
visible=True,
all_visible=True,
on_destroy=lambda *_: app.quit(),
)
app = Application("bluetooth-widget", bluetooth_widget)
app.run()
+26
View File
@@ -0,0 +1,26 @@
from fabric import Application
from fabric.widgets.box import Box
from fabric.widgets.datetime import DateTime
from fabric.widgets.wayland import WaylandWindow as Window
from fabric.utils import get_relative_path
if __name__ == "__main__":
desktop_widget = Window(
layer="bottom",
anchor="center", # FYI: there's no anchor named "center" (anchor of "" is == to "center")
exclusivity="none",
child=Box(
orientation="v",
children=[
DateTime(formatters=["%A. %d %B"], interval=10000, name="date"),
DateTime(formatters=["%I:%M"], name="clock"),
],
),
all_visible=True,
)
app = Application("desktop-widget", desktop_widget)
app.set_stylesheet_from_file(get_relative_path("./style.css"))
app.run()
+38
View File
@@ -0,0 +1,38 @@
:vars {
--background: #150d16;
--foreground: #f5dbc4;
--color0: #150d16;
--color1: #72448D;
--color2: #9C5995;
--color3: #D5719F;
--color4: #9A62F3;
--color5: #E075DF;
--color6: #F98F9F;
--color7: #f5dbc4;
--color8: #ab9989;
--color9: #72448D;
--color10: #9C5995;
--color11: #D5719F;
--color12: #9A62F3;
--color13: #E075DF;
--color14: #F98F9F;
--color15: #f5dbc4;
}
* {
all: unset;
transition: color 0.5s ease;
color: alpha(var(--foreground), 0.8);
font-family: "Jost*", sans-serif;
font-weight: 700;
}
#clock>label {
font-size: 86px;
font-weight: 900;
}
#date>label {
font-size: 24px;
margin-bottom: -18px;
}
+105
View File
@@ -0,0 +1,105 @@
from fabric import Application
from collections.abc import Iterator
from fabric.widgets.box import Box
from fabric.widgets.entry import Entry
from fabric.widgets.image import Image
from fabric.widgets.button import Button
from fabric.widgets.window import Window
from fabric.widgets.flowbox import FlowBox
from fabric.widgets.scrolledwindow import ScrolledWindow
from fabric.utils import idle_add, remove_handler
from gi.repository import Gtk
class IconBrowser(Window):
def __init__(self, **kwargs):
super().__init__(
layer="top",
anchor="center",
exclusivity="none",
keyboard_mode="on-demand",
visible=False,
all_visible=False,
on_destroy=lambda *_: app.quit(),
**kwargs,
)
self._arranger_handler: int = 0
self._icon_theme = Gtk.IconTheme.get_default()
self._all_icons: list[str] = self._icon_theme.list_icons()
self.viewport = FlowBox(orientation="v")
self.search_entry = Entry(
placeholder="Search Icons...",
h_expand=True,
notify_text=lambda entry, *_: self.arrange_viewport(entry.get_text()),
)
self.scrolled_window = ScrolledWindow(
min_content_size=(280, 320), child=self.viewport
)
self.add(
Box(
spacing=2,
orientation="v",
style="margin: 2px",
children=[
# the header with the search entry
Box(
spacing=2,
orientation="h",
children=[
self.search_entry,
Button(
image=Image(icon_name="window-close"),
tooltip_text="Exit",
on_clicked=lambda *_: self.application.quit(),
),
],
),
# the actual slots holder
self.scrolled_window,
],
)
)
self.show_all()
def arrange_viewport(self, query: str = ""):
if self._arranger_handler:
remove_handler(self._arranger_handler)
self.viewport.children = []
self._arranger_handler = idle_add(
self.add_next_icon,
iter(
sorted(
icon_name
for icon_name in self._all_icons
if query.casefold() in icon_name.casefold()
)
),
pin=True,
)
return False
def add_next_icon(self, icons_iter: Iterator[str]):
if not (icon_name := next(icons_iter, None)):
return False
self.viewport.add(
Button(
child=Image(
icon_name=icon_name, icon_size=32, h_align="center", size=32
),
tooltip_text=icon_name,
)
)
return True
if __name__ == "__main__":
icon_browser = IconBrowser()
app = Application("icon-browser", icon_browser)
app.run()
+162
View File
@@ -0,0 +1,162 @@
from typing import cast
from fabric import Application
from fabric.widgets.box import Box
from fabric.widgets.label import Label
from fabric.widgets.image import Image
from fabric.widgets.button import Button
from fabric.widgets.wayland import WaylandWindow
from fabric.notifications import Notifications, Notification
from fabric.utils import invoke_repeater, get_relative_path
from gi.repository import GdkPixbuf
NOTIFICATION_WIDTH = 360
NOTIFICATION_IMAGE_SIZE = 64
NOTIFICATION_TIMEOUT = 10 * 1000 # 10 seconds
class NotificationWidget(Box):
def __init__(self, notification: Notification, **kwargs):
super().__init__(
size=(NOTIFICATION_WIDTH, -1),
name="notification",
spacing=8,
orientation="v",
**kwargs,
)
self._notification = notification
body_container = Box(spacing=4, orientation="h")
if image_pixbuf := self._notification.image_pixbuf:
body_container.add(
Image(
pixbuf=image_pixbuf.scale_simple(
NOTIFICATION_IMAGE_SIZE,
NOTIFICATION_IMAGE_SIZE,
GdkPixbuf.InterpType.BILINEAR,
)
)
)
body_container.add(
Box(
spacing=4,
orientation="v",
children=[
# a box for holding both the "summary" label and the "close" button
Box(
orientation="h",
children=[
Label(
label=self._notification.summary,
ellipsization="middle",
)
.build()
.add_style_class("summary")
.unwrap(),
],
h_expand=True,
v_expand=True,
)
# add the "close" button
.build(
lambda box, _: box.pack_end(
Button(
image=Image(
icon_name="close-symbolic",
icon_size=18,
),
v_align="center",
h_align="end",
on_clicked=lambda *_: self._notification.close(),
),
False,
False,
0,
)
),
Label(
label=self._notification.body,
line_wrap="word-char",
v_align="start",
h_align="start",
)
.build()
.add_style_class("body")
.unwrap(),
],
h_expand=True,
v_expand=True,
)
)
self.add(body_container)
if actions := self._notification.actions:
self.add(
Box(
spacing=4,
orientation="h",
children=[
Button(
h_expand=True,
v_expand=True,
label=action.label,
on_clicked=lambda *_, action=action: action.invoke(),
)
for action in actions
],
)
)
# destroy this widget once the notification is closed
self._notification.connect(
"closed",
lambda *_: (
parent.remove(self) if (parent := self.get_parent()) else None, # type: ignore
self.destroy(),
),
)
# automatically close the notification after the timeout period
invoke_repeater(
NOTIFICATION_TIMEOUT,
lambda: self._notification.close("expired"),
initial_call=False,
)
if __name__ == "__main__":
app = Application(
"notifications",
WaylandWindow(
margin="8px 8px 8px 8px",
anchor="top right",
child=Box(
size=2, # so it's not ignored by the compositor
spacing=4,
orientation="v",
).build(
lambda viewport, _: Notifications(
on_notification_added=lambda notifs_service, nid: viewport.add(
NotificationWidget(
cast(
Notification,
notifs_service.get_notification_from_id(nid),
)
)
)
)
),
visible=True,
all_visible=True,
),
)
app.set_stylesheet_from_file(get_relative_path("./style.css"))
app.run()
+40
View File
@@ -0,0 +1,40 @@
:vars {
--foreground: rgb(255, 255, 255);
--background: rgb(26, 28, 30);
--border-color: rgb(42, 43, 45);
--button-color: var(--border-color);
}
* {
all: unset;
color: var(--foreground);
font-family: "Jost*", "Cantarell", "Helvetica", "Arial", sans-serif;
}
#notification {
padding: 0.8rem;
border: solid 1px var(--border-color);
border-radius: 1rem;
background-color: var(--background);
}
#notification .summary {
font-size: 20px;
font-weight: bold;
}
#notification .body {
color: darker(var(--foreground));
font-weight: normal;
}
button {
padding: 0.6rem;
font-weight: 600;
border-radius: 3rem;
background-color: var(--button-color);
}
button:hover {
background-color: lighter(var(--button-color));
}
+174
View File
@@ -0,0 +1,174 @@
"""side panel example, contains info about the system"""
import os
import time
import psutil
from loguru import logger
from fabric import Application
from fabric.widgets.box import Box
from fabric.widgets.label import Label
from fabric.widgets.overlay import Overlay
from fabric.widgets.datetime import DateTime
from fabric.widgets.circularprogressbar import CircularProgressBar
from fabric.widgets.wayland import WaylandWindow as Window
from fabric.utils import invoke_repeater, get_relative_path
def get_profile_picture_path() -> str | None:
path = os.path.expanduser("~/Pictures/Other/face.jpg")
if not os.path.exists(path):
path = os.path.expanduser("~/.face")
if not os.path.exists(path):
logger.warning(
"can't fetch a user profile picture, add a profile picture image at ~/.face or at ~/Pictures/Other/profile.jpg"
)
path = None
return path
class SidePanel(Window):
@staticmethod
def bake_progress_bar(name: str = "progress-bar", size: int = 64, **kwargs):
return CircularProgressBar(
name=name, min_value=0, max_value=100, size=size, **kwargs
)
@staticmethod
def bake_progress_icon(**kwargs):
return Label(**kwargs).build().add_style_class("progress-icon").unwrap()
def __init__(self, **kwargs):
super().__init__(
layer="overlay",
title="fabric-overlay",
anchor="top right",
margin="10px 10px 10px 0px",
exclusivity="none",
visible=False,
all_visible=False,
**kwargs,
)
self.profile_pic = Box(
name="profile-pic",
style=f"background-image: url(\"file://{get_profile_picture_path() or ''}\")",
)
self.uptime_label = Label(label=f"{self.get_current_uptime()}")
self.header = Box(
spacing=14,
name="header",
orientation="h",
children=[
self.profile_pic,
Box(
orientation="v",
children=[
DateTime(
name="date-time",
style="margin-top: 4px; min-width: 180px;",
),
self.uptime_label,
],
),
],
)
self.greeter_label = Label(
label=f"Good {'Morning' if time.localtime().tm_hour < 12 else 'Afternoon'}, {os.getlogin().title()}!",
style="font-size: 20px;",
)
self.cpu_progress = self.bake_progress_bar()
self.ram_progress = self.bake_progress_bar()
self.bat_circular = self.bake_progress_bar().build().set_value(42).unwrap()
self.progress_container = Box(
name="progress-bar-container",
spacing=12,
children=[
Box(
children=[
Overlay(
child=self.cpu_progress,
overlays=[
self.bake_progress_icon(
label="",
style="margin-right: 8px; text-shadow: 0 0 10px #fff, 0 0 10px #fff, 0 0 10px #fff;",
)
],
),
],
),
Box(name="progress-bar-sep"),
Box(
children=[
Overlay(
child=self.ram_progress,
overlays=[
self.bake_progress_icon(
label="󰘚",
style="margin-right: 4px; text-shadow: 0 0 10px #fff;",
)
],
)
]
),
Box(name="progress-bar-sep"),
Box(
children=[
Overlay(
child=self.bat_circular,
overlays=[
self.bake_progress_icon(
label="󱊣",
style="margin-right: 0px; text-shadow: 0 0 10px #fff, 0 0 18px #fff;",
)
],
),
],
),
],
)
self.update_status()
invoke_repeater(
15 * 60 * 1000, # every 15min
lambda: (self.uptime_label.set_label(self.get_current_uptime()), True)[1],
)
invoke_repeater(1000, self.update_status)
self.add(
Box(
name="window-inner",
orientation="v",
spacing=24,
children=[self.header, self.greeter_label, self.progress_container],
),
)
self.show_all()
def update_status(self):
self.cpu_progress.value = psutil.cpu_percent()
self.ram_progress.value = psutil.virtual_memory().percent
if not (bat_sen := psutil.sensors_battery()):
self.bat_circular.value = 42
else:
self.bat_circular.value = bat_sen.percent
return True
def get_current_uptime(self):
uptime = time.time() - psutil.boot_time()
uptime_days, remainder = divmod(uptime, 86400)
uptime_hours, remainder = divmod(remainder, 3600)
# uptime_minutes, _ = divmod(remainder, 60)
return f"{int(uptime_days)} {'days' if uptime_days > 1 else 'day'}, {int(uptime_hours)} {'hours' if uptime_hours > 1 else 'hour'}"
if __name__ == "__main__":
side_panel = SidePanel()
app = Application("side-panel", side_panel)
app.set_stylesheet_from_file(get_relative_path("./style.css"))
app.run()
+83
View File
@@ -0,0 +1,83 @@
:vars {
--background: #150d16;
--foreground: #f5dbc4;
--color0: #150d16;
--color1: #72448D;
--color2: #9C5995;
--color3: #D5719F;
--color4: #9A62F3;
--color5: #E075DF;
--color6: #F98F9F;
--color7: #f5dbc4;
--color8: #ab9989;
--color9: #72448D;
--color10: #9C5995;
--color11: #D5719F;
--color12: #9A62F3;
--color13: #E075DF;
--color14: #F98F9F;
--color15: #f5dbc4;
--window-bg: alpha(var(--background), 0.6);
--module-bg: alpha(var(--background), 0.4);
--border-color: var(--color11);
}
* {
all: unset;
font-family: "Jost*";
font-weight: 500;
font-size: 18px;
color: var(--foreground);
}
#window-inner {
border: solid 2px;
border-color: var(--color11);
background-color: var(--window-bg);
border-radius: 100px;
border-radius: 12px;
padding: 10px;
}
#date-time>label {
font-weight: 900;
font-size: 28px;
}
#header {
border: solid 1px var(--border-color);
border-radius: 8px;
box-shadow: 0px 18px 23px -6px rgba(0, 0, 0, 0.75);
background: linear-gradient(90deg,
alpha(var(--color11), 0.2),
alpha(var(--background), 0.2),
var(--module-bg));
}
#profile-pic {
background-color: alpha(var(--color11), 0.2);
background-position: center;
background-repeat: no-repeat;
background-size: cover;
border-radius: 6px;
padding: 32px;
margin: 4px;
}
#progress-bar {
color: var(--module-bg);
border: solid 10px var(--border-color);
}
#progress-bar-sep {
padding: 4px;
margin: 16px 0px;
border-radius: 100px;
background-color: var(--module-bg);
}
#progress-bar-container {
background-color: var(--module-bg);
border-radius: 100px;
padding: 8px;
}