From e4b8fe3b94f1228d7c842c4efd8f4bb2ec285b99 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Fri, 4 Jul 2025 12:41:41 +0545 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=85=20style(examples/app-launcher):=20?= =?UTF-8?q?update=20indentation=20and=20spacing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/app-launcher/config.py | 149 ------------ examples/bar/config.py | 170 ------------- examples/bar/style.css | 169 ------------- examples/bar/wayland.py | 359 ---------------------------- examples/bluetooth-widget/config.py | 112 --------- examples/desktop-widget/config.py | 26 -- examples/desktop-widget/style.css | 38 --- examples/icon-browser/config.py | 105 -------- examples/notifications/config.py | 162 ------------- examples/notifications/style.css | 40 ---- examples/side-panel/config.py | 174 -------------- examples/side-panel/style.css | 83 ------- 12 files changed, 1587 deletions(-) delete mode 100644 examples/app-launcher/config.py delete mode 100644 examples/bar/config.py delete mode 100644 examples/bar/style.css delete mode 100644 examples/bar/wayland.py delete mode 100644 examples/bluetooth-widget/config.py delete mode 100644 examples/desktop-widget/config.py delete mode 100644 examples/desktop-widget/style.css delete mode 100644 examples/icon-browser/config.py delete mode 100644 examples/notifications/config.py delete mode 100644 examples/notifications/style.css delete mode 100644 examples/side-panel/config.py delete mode 100644 examples/side-panel/style.css diff --git a/examples/app-launcher/config.py b/examples/app-launcher/config.py deleted file mode 100644 index dd5640e..0000000 --- a/examples/app-launcher/config.py +++ /dev/null @@ -1,149 +0,0 @@ -""" -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() diff --git a/examples/bar/config.py b/examples/bar/config.py deleted file mode 100644 index 7265629..0000000 --- a/examples/bar/config.py +++ /dev/null @@ -1,170 +0,0 @@ -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() diff --git a/examples/bar/style.css b/examples/bar/style.css deleted file mode 100644 index 695a1fa..0000000 --- a/examples/bar/style.css +++ /dev/null @@ -1,169 +0,0 @@ -/* 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 -} diff --git a/examples/bar/wayland.py b/examples/bar/wayland.py deleted file mode 100644 index 0dcbe51..0000000 --- a/examples/bar/wayland.py +++ /dev/null @@ -1,359 +0,0 @@ -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], - } diff --git a/examples/bluetooth-widget/config.py b/examples/bluetooth-widget/config.py deleted file mode 100644 index cbd3fc5..0000000 --- a/examples/bluetooth-widget/config.py +++ /dev/null @@ -1,112 +0,0 @@ -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() diff --git a/examples/desktop-widget/config.py b/examples/desktop-widget/config.py deleted file mode 100644 index ac68b7f..0000000 --- a/examples/desktop-widget/config.py +++ /dev/null @@ -1,26 +0,0 @@ -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() diff --git a/examples/desktop-widget/style.css b/examples/desktop-widget/style.css deleted file mode 100644 index 936c1e4..0000000 --- a/examples/desktop-widget/style.css +++ /dev/null @@ -1,38 +0,0 @@ -: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; -} diff --git a/examples/icon-browser/config.py b/examples/icon-browser/config.py deleted file mode 100644 index 482eb97..0000000 --- a/examples/icon-browser/config.py +++ /dev/null @@ -1,105 +0,0 @@ -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() diff --git a/examples/notifications/config.py b/examples/notifications/config.py deleted file mode 100644 index de4995a..0000000 --- a/examples/notifications/config.py +++ /dev/null @@ -1,162 +0,0 @@ -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() diff --git a/examples/notifications/style.css b/examples/notifications/style.css deleted file mode 100644 index 2954097..0000000 --- a/examples/notifications/style.css +++ /dev/null @@ -1,40 +0,0 @@ -: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)); -} diff --git a/examples/side-panel/config.py b/examples/side-panel/config.py deleted file mode 100644 index b15d577..0000000 --- a/examples/side-panel/config.py +++ /dev/null @@ -1,174 +0,0 @@ -"""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() diff --git a/examples/side-panel/style.css b/examples/side-panel/style.css deleted file mode 100644 index dd46df6..0000000 --- a/examples/side-panel/style.css +++ /dev/null @@ -1,83 +0,0 @@ -: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; -}