🔧 - 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:
14 files changed
+1596
-12
No files matched your search
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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
|
||||
}
|
||||
@@ -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],
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user