[SCRIPT] Updated

This commit is contained in:
t
2025-03-31 20:14:11 +02:00
parent 810f54cb65
commit 314a27eb42
472 changed files with 11721 additions and 921 deletions
@@ -0,0 +1,25 @@
from ignis.widgets import Widget
from .workspaces import workspaces
from .kb_layout import kb_layout
from .clock import clock
from .pinned_apps import pinned_apps
from .tray import tray
from .battery import battery_widget
# from .wallpaper_engine import wallpaper_engine_widget
def bar(monitor: int) -> Widget.Window:
return Widget.Window(
anchor=["left", "top", "right"],
exclusivity="exclusive",
monitor=monitor,
namespace=f"ignis_BAR_{monitor}",
layer="top",
kb_mode="none",
child=Widget.CenterBox(
css_classes=["bar-widget"],
start_widget=Widget.Box(child=[workspaces()]),
center_widget=Widget.Box(child=[pinned_apps()]),
end_widget=Widget.Box(child=[tray(), kb_layout(), battery_widget(), clock(monitor)]),
), # wallpaper_engine_widget(),
css_classes=["unset"],
)
@@ -0,0 +1,35 @@
from ignis.widgets import Widget
from ignis.services.upower import UPowerService, UPowerDevice
upower = UPowerService.get_default()
def battery_item(device: UPowerDevice) -> Widget.Box:
return Widget.Box(
css_classes=["battery-item"],
setup=lambda self: device.connect("removed", lambda x: self.unparent()),
child=[
Widget.Icon(
icon_name=device.bind("icon_name"), css_classes=["battery-icon"]
),
Widget.Label(
label=device.bind("percent", lambda x: f"{int(x)}%"),
css_classes=["battery-percent"],
),
Widget.Scale(
min=0,
max=100,
value=device.bind("percent"),
sensitive=False,
css_classes=["battery-scale"],
),
],
)
def battery_widget() -> Widget.Box:
return Widget.Box(
setup=lambda self: upower.connect(
"battery-added", lambda x, device: self.append(battery_item(device))
),
)
@@ -0,0 +1,41 @@
import datetime
from ignis.widgets import Widget
from ignis.app import IgnisApp
from ignis.utils import Utils
from ignis.variable import Variable
from .indicator import status_icons
app = IgnisApp.get_default()
current_time = Variable(
value=Utils.Poll(1000, lambda x: datetime.datetime.now().strftime("%H:%M:%S")).bind(
"output"
)
)
def clock(monitor):
window: Widget.Window = app.get_window("ignis_CONTROL_CENTER") # type: ignore
def on_click(x):
if window.monitor == monitor:
window.visible = not window.visible
else:
window.set_monitor(monitor)
window.visible = True
return Widget.Button(
child=Widget.Box(
child=[
status_icons(),
Widget.Label(
label=current_time.bind("value"),
),
]
),
css_classes=window.bind(
"visible",
lambda value: ["clock", "unset", "active"] if value else ["clock", "unset"],
),
on_click=on_click,
)
@@ -0,0 +1,92 @@
from ignis.widgets import Widget
from ignis.services.network import NetworkService
from ignis.services.notifications import NotificationService
from ignis.services.recorder import RecorderService
from ignis.services.audio import AudioService
network = NetworkService.get_default()
notifications = NotificationService.get_default()
recorder = RecorderService.get_default()
audio = AudioService.get_default()
def indicator_icon(**kwargs):
return Widget.Icon(style="margin-right: 0.5rem;", css_classes=["unset"], **kwargs)
def wifi_icon():
def check_visible(*args) -> bool:
if len(network.wifi.devices) > 0:
if network.ethernet.is_connected:
if network.wifi.is_connected:
return True
else:
return False
else:
return True
else:
return False
icon = indicator_icon(image=network.wifi.bind("icon-name"))
icon.visible = network.wifi.bind("devices", check_visible)
icon.visible = network.ethernet.bind("is_connected", check_visible)
icon.visible = network.wifi.bind("is_connected", check_visible)
return icon
def ethernet_icon():
def check_visible(*args) -> bool:
if len(network.ethernet.devices) > 0:
if network.wifi.is_connected:
if network.ethernet.is_connected:
return True
else:
return False
else:
return True
else:
return False
icon = indicator_icon(image=network.ethernet.bind("icon_name"))
icon.visible = network.ethernet.bind("devices", check_visible)
icon.visible = network.wifi.bind("is_connected", check_visible)
icon.visible = network.ethernet.bind("is_connected", check_visible)
return icon
def dnd_icon():
return indicator_icon(
image="notification-disabled-symbolic",
visible=notifications.bind("dnd"),
)
def recorder_icon():
def check_state(icon: Widget.Icon) -> None:
if recorder.is_paused:
icon.remove_css_class("active")
else:
icon.add_css_class("active")
icon = indicator_icon(
image="media-record-symbolic",
visible=recorder.bind("active"),
)
icon.add_css_class("record-indicator")
recorder.connect("notify::is-paused", lambda x, y: check_state(icon))
return icon
def volume_icon():
return indicator_icon(
image=audio.speaker.bind("icon_name"),
)
def status_icons():
return Widget.Box(
child=[recorder_icon(), wifi_icon(), ethernet_icon(), volume_icon(), dnd_icon()]
)
@@ -0,0 +1,21 @@
from ignis.widgets import Widget
from ignis.exceptions import HyprlandIPCNotFoundError
from ignis.services.hyprland import HyprlandService
try:
hyprland = HyprlandService.get_default()
def kb_layout():
return Widget.Button(
css_classes=["kb-layout", "unset"],
on_click=lambda x: hyprland.switch_kb_layout(),
child=Widget.Label(
label=hyprland.bind(
"kb_layout", transform=lambda value: value[:2].lower()
)
),
)
except HyprlandIPCNotFoundError:
def kb_layout():
return
@@ -0,0 +1,51 @@
from ignis.widgets import Widget
from ignis.app import IgnisApp
from ignis.services.applications import ApplicationsService
applications = ApplicationsService.get_default()
app = IgnisApp.get_default()
class AppItem(Widget.Button):
def __init__(self, app):
menu = Widget.PopoverMenu(
items=[
Widget.MenuItem(label="Launch", on_activate=lambda x: app.launch()),
Widget.Separator(),
]
+ [
Widget.MenuItem(
label=i.name, on_activate=lambda x, action=i: action.launch()
)
for i in app.actions
]
+ [
Widget.Separator(),
Widget.MenuItem(label="Unpin", on_activate=lambda x: app.unpin()),
]
)
super().__init__(
child=Widget.Box(child=[Widget.Icon(image=app.icon, pixel_size=32), menu]),
on_click=lambda x: app.launch(),
on_right_click=lambda x: menu.popup(),
css_classes=["pinned-app", "unset"],
)
def launcher_button():
return Widget.Button(
child=Widget.Icon(image="start-here-symbolic", pixel_size=32),
on_click=lambda x: app.toggle_window("ignis_LAUNCHER"),
css_classes=["pinned-app", "unset"],
)
def pinned_apps():
return Widget.Box(
child=applications.bind(
"pinned",
transform=lambda value: [AppItem(app) for app in value]
+ [launcher_button()],
)
)
@@ -0,0 +1,37 @@
from ignis.widgets import Widget
from ignis.services.system_tray import SystemTrayService
system_tray = SystemTrayService.get_default()
class TrayItem(Widget.Button):
def __init__(self, item):
if item.menu:
menu = item.menu.copy()
else:
menu = None
super().__init__(
child=Widget.Box(
child=[
Widget.Icon(image=item.bind("icon"), pixel_size=24),
menu,
]
),
tooltip_text=item.bind("tooltip"),
on_click=lambda x: item.activate(),
on_right_click=lambda x: menu.popup() if menu else None,
css_classes=["tray-item", "unset"],
)
item.connect("removed", lambda x: self.unparent())
def tray():
return Widget.Box(
css_classes=["tray"],
setup=lambda self: system_tray.connect(
"added", lambda x, item: self.append(TrayItem(item))
),
spacing=10,
)
@@ -0,0 +1,25 @@
from ignis.widgets import Widget
from ignis.exceptions import HyprlandIPCNotFoundError
from ignis.services.hyprland import HyprlandService
import os
def toggle_engine():
os.system("cd /home/timo/.config/hypr/linux-wallpaperengine && ./linux-wallpaperengine 2657260174 --screen-root DP-3")
try:
hyprland = HyprlandService.get_default()
def wallpaper_engine_widget():
return Widget.Button(
css_classes=["kb-layout", "unset"],
on_click=lambda x: os.system("cd /home/timo/.config/hypr/linux-wallpaperengine && ./linux-wallpaperengine 2657260174 --screen-root DP-3"),
child=Widget.Label(
label=hyprland.bind(
"kb_layout", transform=lambda value: value[:2].lower()
)
),
)
except HyprlandIPCNotFoundError:
def kb_layout():
return
@@ -0,0 +1,46 @@
from ignis.widgets import Widget
from ignis.exceptions import HyprlandIPCNotFoundError
from ignis.services.hyprland import HyprlandService
class WorkspaceButton(Widget.Button):
def __init__(self, workspace: dict) -> None:
super().__init__(
css_classes=["workspace", "unset"],
on_click=lambda x, id=workspace["id"]: hyprland.switch_to_workspace(id),
halign="start",
valign="center",
)
if workspace["id"] == hyprland.active_workspace["id"]:
self.add_css_class("active")
try:
hyprland = HyprlandService.get_default()
def scroll_workspaces(direction: str) -> None:
current = hyprland.active_workspace["id"]
if direction == "up":
target = current - 1
hyprland.switch_to_workspace(target)
else:
target = current + 1
if target == 11:
return
hyprland.switch_to_workspace(target)
def workspaces():
return Widget.EventBox(
on_scroll_up=lambda x: scroll_workspaces("up"),
on_scroll_down=lambda x: scroll_workspaces("down"),
css_classes=["workspaces"],
child=hyprland.bind(
"workspaces",
transform=lambda value: [WorkspaceButton(i) for i in value],
),
)
except HyprlandIPCNotFoundError:
def workspaces():
return
@@ -0,0 +1,62 @@
from ignis.widgets import Widget
from ignis.app import IgnisApp
from .volume import volume_control
from .quick_settings import quick_settings
from .user import user
from .media import media
from .notification_center import notification_center
from .brightness import brightness_slider
app = IgnisApp.get_default()
def control_center_widget() -> Widget.Box:
return Widget.Box(
vertical=True,
css_classes=["control-center"],
child=[
Widget.Box(
vertical=True,
css_classes=["control-center-widget"],
child=[
quick_settings(),
volume_control(),
brightness_slider(),
user(),
media(),
],
),
notification_center(),
],
)
def control_center() -> Widget.RevealerWindow:
revealer = Widget.Revealer(
transition_type="slide_left",
child=control_center_widget(),
transition_duration=300,
reveal_child=True,
)
box = Widget.Box(
child=[
Widget.Button(
vexpand=True,
hexpand=True,
css_classes=["unset"],
on_click=lambda x: app.close_window("ignis_CONTROL_CENTER"),
),
revealer,
],
)
return Widget.RevealerWindow(
visible=False,
popup=True,
kb_mode="on_demand",
layer="top",
css_classes=["unset"],
anchor=["top", "right", "bottom", "left"],
namespace="ignis_CONTROL_CENTER",
child=box,
revealer=revealer,
)
@@ -0,0 +1,27 @@
from ignis.services.backlight import BacklightService
from ignis.widgets import Widget
backlight = BacklightService.get_default()
def brightness_slider() -> Widget.Scale:
return Widget.Box(
visible=backlight.bind("available"),
hexpand=True,
style="margin-top: 0.25rem;",
child=[
Widget.Icon(
image="display-brightness-symbolic",
css_classes=["material-slider-icon"],
pixel_size=18,
),
Widget.Scale(
min=0,
max=backlight.max_brightness,
hexpand=True,
value=backlight.bind("brightness"),
css_classes=["material-slider"],
on_change=lambda x: backlight.set_brightness(x.value),
),
],
)
@@ -0,0 +1,14 @@
from services.material import MaterialService
from .qs_button import QSButton
material = MaterialService.get_default()
def dark_mode_button() -> QSButton:
return QSButton(
label="Dark",
icon_name="night-light-symbolic",
on_activate=lambda x: material.set_dark_mode(True),
on_deactivate=lambda x: material.set_dark_mode(False),
active=material.bind("dark-mode"),
)
@@ -0,0 +1,19 @@
from .qs_button import QSButton
from ignis.services.notifications import NotificationService
notifications = NotificationService.get_default()
def dnd_button() -> QSButton:
return QSButton(
label=notifications.bind("dnd", lambda value: "Silent" if value else "Noisy"),
icon_name=notifications.bind(
"dnd",
transform=lambda value: "notification-disabled-symbolic"
if value
else "notification-symbolic",
),
on_activate=lambda x: notifications.set_dnd(not notifications.dnd),
on_deactivate=lambda x: notifications.set_dnd(not notifications.dnd),
active=notifications.bind("dnd"),
)
@@ -0,0 +1,100 @@
from typing import List
from ignis.widgets import Widget
from ignis.utils import Utils
from .qs_button import QSButton
from ignis.services.network import NetworkService, EthernetDevice
network = NetworkService.get_default()
class EthernetConnectionItem(Widget.Button):
def __init__(self, device: EthernetDevice):
super().__init__(
css_classes=["ethernet-connection-item", "unset"],
on_click=lambda x: device.disconnect_from()
if device.is_connected
else device.connect_to(),
child=Widget.Box(
child=[
Widget.Icon(image="network-wired-symbolic"),
Widget.Label(
label=device.name,
ellipsize="end",
max_width_chars=20,
halign="start",
css_classes=["ethernet-connection-label"],
),
Widget.Button(
child=Widget.Label(
label=device.bind(
"is_connected",
lambda value: "Disconnect" if value else "Connect",
)
),
css_classes=["ethernet-connection-item-connect-label", "unset"],
halign="end",
hexpand=True,
),
]
),
)
def ethernet_control() -> List[QSButton]:
networks_list = Widget.Revealer(
transition_duration=300,
transition_type="slide_down",
child=Widget.Box(
vertical=True,
css_classes=["control-center-menu"],
child=[
Widget.Box(
css_classes=["ethernet-header-box"],
child=[
Widget.Icon(icon_name="network-wired-symbolic", pixel_size=28),
Widget.Label(
label="Wired connections",
css_classes=["ethernet-header-label"],
),
],
),
Widget.Box(
vertical=True,
child=network.ethernet.bind(
"devices",
lambda value: [EthernetConnectionItem(i) for i in value],
),
),
Widget.Separator(css_classes=["ethernet-network-list-separator"]),
Widget.Button(
css_classes=["ethernet-connection-item", "unset"],
style="margin-bottom: 0;",
on_click=lambda x: Utils.exec_sh_async("nm-connection-editor"),
child=Widget.Box(
child=[
Widget.Icon(image="preferences-system-symbolic"),
Widget.Label(
label="Network Settings",
halign="start",
css_classes=["ethernet-connection-label"],
),
]
),
),
],
),
)
if len(network.ethernet.devices) > 0:
return [
QSButton(
label="Wired",
icon_name="network-wired-symbolic",
on_activate=lambda x: networks_list.toggle(),
on_deactivate=lambda x: networks_list.toggle(),
content=networks_list,
active=network.ethernet.bind("is_connected"),
)
]
else:
return []
@@ -0,0 +1,202 @@
import os
import ignis
from ignis.widgets import Widget
from ignis.services.mpris import MprisService, MprisPlayer
from ignis.utils import Utils
from services.material import MaterialService
from ignis.app import IgnisApp
from ignis.exceptions import StylePathNotFoundError
mpris = MprisService.get_default()
app = IgnisApp.get_default()
material = MaterialService.get_default()
MEDIA_TEMPLATE = Utils.get_current_dir() + "/../../scss/media.scss"
MEDIA_SCSS_CACHE_DIR = ignis.CACHE_DIR + "/media" # type: ignore
MEDIA_ART_FALLBACK = Utils.get_current_dir() + "/../../misc/media-art-fallback.png"
os.makedirs(MEDIA_SCSS_CACHE_DIR, exist_ok=True)
PLAYER_ICONS = {"spotify": "󰓇", "firefox": "󰈹", "chrome": "󰊯", None: ""}
class Player(Widget.Revealer):
def __init__(self, player: MprisPlayer) -> None:
self._player = player
self._colors_path = f"{MEDIA_SCSS_CACHE_DIR}/{self._player.desktop_entry}.scss"
player.connect("closed", lambda x: self.destroy())
player.connect("notify::art-url", lambda x, y: self.load_colors())
self.load_colors()
super().__init__(
transition_type="slide_down",
reveal_child=False,
css_classes=[self.get_css("media")],
child=Widget.Overlay(
child=Widget.Box(css_classes=[self.get_css("media-image")]),
overlays=[
Widget.Box(
hexpand=True,
vexpand=True,
css_classes=[self.get_css("media-image-gradient")],
),
Widget.Label(
label=self.get_player_icon(),
halign="start",
valign="start",
css_classes=[self.get_css("media-player-icon")],
),
Widget.Box(
vertical=True,
hexpand=True,
css_classes=[self.get_css("media-content")],
child=[
Widget.Box(
vexpand=True,
valign="center",
child=[
Widget.Box(
hexpand=True,
vertical=True,
child=[
Widget.Label(
ellipsize="end",
label=player.bind("title"),
max_width_chars=30,
halign="start",
css_classes=[
self.get_css("media-title")
],
),
Widget.Label(
label=player.bind("artist"),
max_width_chars=30,
ellipsize="end",
halign="start",
css_classes=[
self.get_css("media-artist")
],
),
],
),
Widget.Button(
child=Widget.Icon(
image=player.bind(
"playback_status",
lambda value: "media-playback-pause-symbolic"
if value == "Playing"
else "media-playback-start-symbolic",
),
pixel_size=18,
),
on_click=lambda x: player.play_pause(),
visible=player.bind("can_play"),
css_classes=player.bind(
"playback_status",
lambda value: [
self.get_css("media-playback-button"),
"playing",
]
if value == "Playing"
else [
self.get_css("media-playback-button"),
"paused",
],
),
),
],
),
],
),
Widget.Box(
vexpand=True,
valign="end",
style="padding: 1rem;",
child=[
Widget.Scale(
value=player.bind("position"),
max=player.bind("length"),
hexpand=True,
css_classes=[self.get_css("media-scale")],
on_change=lambda x: player.set_position(x.value),
visible=player.bind(
"position", lambda value: value != -1
),
),
Widget.Button(
child=Widget.Icon(
image="media-skip-backward-symbolic",
pixel_size=20,
),
css_classes=[self.get_css("media-skip-button")],
on_click=lambda x: player.previous(),
visible=player.bind("can_go_previous"),
style="margin-left: 1rem;",
),
Widget.Button(
child=Widget.Icon(
image="media-skip-forward-symbolic",
pixel_size=20,
),
css_classes=[self.get_css("media-skip-button")],
on_click=lambda x: player.next(),
visible=player.bind("can_go_next"),
style="margin-left: 1rem;",
),
],
),
],
),
)
def get_player_icon(self) -> str:
if self._player.desktop_entry == "firefox":
return PLAYER_ICONS["firefox"]
elif self._player.desktop_entry == "spotify":
return PLAYER_ICONS["spotify"]
elif "chromium" in self._player.track_id or "chrome" in self._player.track_id:
return PLAYER_ICONS["chrome"]
else:
return PLAYER_ICONS[None]
def destroy(self) -> None:
self.set_reveal_child(False)
Utils.Timeout(self.transition_duration, super().unparent)
def get_css(self, class_name: str) -> str:
return f"{class_name}-{self._player.desktop_entry}"
def load_colors(self) -> None:
if not self._player.art_url:
art_url = MEDIA_ART_FALLBACK
else:
art_url = self._player.art_url
try:
app.remove_css(self._colors_path)
except StylePathNotFoundError:
pass
colors = material.get_colors_from_img(art_url, True)
colors["art_url"] = art_url
colors["desktop_entry"] = self._player.desktop_entry
material.render_template(
colors, input_file=MEDIA_TEMPLATE, output_file=self._colors_path
)
app.apply_css(self._colors_path)
def media() -> Widget.Box:
def add_player(box: Widget.Box, obj: MprisPlayer) -> None:
player = Player(obj)
box.append(player)
player.set_reveal_child(True)
return Widget.Box(
vertical=True,
setup=lambda self: mpris.connect(
"player_added", lambda x, player: add_player(self, player)
),
css_classes=["rec-unset"],
)
@@ -0,0 +1,242 @@
from ignis.widgets import Widget
from ignis.services.notifications import Notification, NotificationService
from ignis.utils import Utils
from gi.repository import GLib # type: ignore
notifications = NotificationService.get_default()
class ScreenshotLayout(Widget.Box):
def __init__(self, notification: Notification) -> None:
super().__init__(
vertical=True,
hexpand=True,
child=[
Widget.Box(
child=[
Widget.Picture(
image=notification.icon,
content_fit="cover",
width=1920 // 7,
height=1080 // 7,
style="border-radius: 1rem; background-color: black;",
),
Widget.Button(
child=Widget.Icon(
image="window-close-symbolic", pixel_size=20
),
halign="end",
valign="start",
hexpand=True,
css_classes=["notification-close"],
on_click=lambda x: notification.close(),
),
],
),
Widget.Label(
label="Screenshot saved",
css_classes=["notification-screenshot-label"],
),
Widget.Box(
homogeneous=True,
style="margin-top: 0.75rem;",
spacing=10,
child=[
Widget.Button(
child=Widget.Label(label="Open"),
css_classes=["notification-action"],
on_click=lambda x: Utils.exec_sh_async(
f"xdg-open {notification.icon}"
),
),
Widget.Button(
child=Widget.Label(label="Close"),
css_classes=["notification-action"],
on_click=lambda x: notification.close(),
),
],
),
],
)
class NormalLayout(Widget.Box):
def __init__(self, notification: Notification) -> None:
super().__init__(
vertical=True,
hexpand=True,
child=[
Widget.Box(
child=[
Widget.Icon(
image=notification.icon
if notification.icon
else "dialog-information-symbolic",
pixel_size=48,
halign="start",
valign="start",
),
Widget.Box(
vertical=True,
style="margin-left: 0.75rem;",
child=[
Widget.Label(
ellipsize="end",
label=notification.summary,
halign="start",
visible=notification.summary != "",
css_classes=["notification-summary"],
),
Widget.Label(
label=notification.body,
ellipsize="end",
halign="start",
css_classes=["notification-body"],
visible=notification.body != "",
),
],
),
Widget.Button(
child=Widget.Icon(
image="window-close-symbolic", pixel_size=20
),
halign="end",
valign="start",
hexpand=True,
css_classes=["notification-close"],
on_click=lambda x: notification.close(),
),
],
),
Widget.Box(
child=[
Widget.Button(
child=Widget.Label(label=action.label),
on_click=lambda x, action=action: action.invoke(),
css_classes=["notification-action"],
)
for action in notification.actions
],
homogeneous=True,
style="margin-top: 0.75rem;" if notification.actions else "",
spacing=10,
),
],
)
class NotificationWidget(Widget.Box):
def __init__(self, notification: Notification) -> None:
layout: NormalLayout | ScreenshotLayout
if notification.app_name == "grimblast":
layout = ScreenshotLayout(notification)
else:
layout = NormalLayout(notification)
super().__init__(
css_classes=["notification"],
child=[layout],
)
class Popup(Widget.Revealer):
def __init__(self, notification: Notification, **kwargs):
widget = NotificationWidget(notification)
super().__init__(child=widget, transition_type="slide_down", **kwargs)
notification.connect("closed", lambda x: self.destroy())
def destroy(self):
self.reveal_child = False
Utils.Timeout(self.transition_duration, self.unparent)
def loading_notif_label() -> Widget.Label:
return Widget.Label(
label="Loading notifications...",
valign="center",
vexpand=True,
css_classes=["notification-center-info-label"],
)
def no_notifications_label() -> Widget.Label:
return Widget.Label(
label="No notifications",
valign="center",
vexpand=True,
visible=notifications.bind("notifications", lambda value: len(value) == 0),
css_classes=["notification-center-info-label"],
)
def on_notified(box: Widget.Box, notification: Notification) -> None:
notify = Popup(notification)
box.prepend(notify)
notify.reveal_child = True
def load_notifications() -> list[Widget.Label | Popup]:
widgets = []
for i in notifications.notifications:
GLib.idle_add(lambda i=i: widgets.append(Popup(i, reveal_child=True)))
return widgets
def notification_list() -> Widget.Box:
box = Widget.Box(
vertical=True,
child=[loading_notif_label()],
vexpand=True,
css_classes=["rec-unset"],
setup=lambda self: notifications.connect(
"notified",
lambda x, notification: on_notified(self, notification),
),
)
Utils.ThreadTask(
load_notifications,
lambda result: box.set_child(result + [no_notifications_label()]),
).run()
return box
def notification_center() -> Widget.Box:
main_box = Widget.Box(
vertical=True,
vexpand=True,
css_classes=["notification-center"],
child=[
Widget.Box(
css_classes=["notification-center-header", "rec-unset"],
child=[
Widget.Label(
label=notifications.bind(
"notifications", lambda value: str(len(value))
),
css_classes=["notification-count"],
),
Widget.Label(
label="notifications",
css_classes=["notification-header-label"],
),
Widget.Button(
child=Widget.Label(label="Clear all"),
halign="end",
hexpand=True,
on_click=lambda x: notifications.clear_all(),
css_classes=["notification-clear-all"],
),
],
),
Widget.Scroll(
child=notification_list(),
vexpand=True,
),
],
)
return main_box
@@ -0,0 +1,64 @@
from ignis.widgets import Widget
from gi.repository import GObject # type: ignore
from typing import Callable
from ignis.gobject import Binding
class QSButton(Widget.Button):
def __init__(
self,
label: str | Binding,
icon_name: str | Binding,
on_activate: Callable | None = None,
on_deactivate: Callable | None = None,
content: Widget.Revealer | None = None,
**kwargs,
):
self.on_activate = on_activate
self.on_deactivate = on_deactivate
self._active = False
self._content = content
super().__init__(
child=Widget.Box(
child=[
Widget.Icon(image=icon_name),
Widget.Label(label=label, css_classes=["qs-button-label"]),
Widget.Arrow(
halign="end",
hexpand=True,
pixel_size=20,
rotated=content.bind("reveal_child"),
)
if content
else None,
]
),
on_click=self.__callback,
css_classes=["qs-button", "unset"],
hexpand=True,
**kwargs,
)
def __callback(self, *args) -> None:
if self.active:
if self.on_deactivate:
self.on_deactivate(self)
else:
if self.on_activate:
self.on_activate(self)
@GObject.Property
def active(self) -> bool:
return self._active
@active.setter
def active(self, value: bool) -> None:
self._active = value
if value:
self.add_css_class("active")
else:
self.remove_css_class("active")
@GObject.Property
def content(self) -> Widget.Revealer | None:
return self._content
@@ -0,0 +1,64 @@
from ignis.widgets import Widget
from .wifi import wifi_control
from .record import record_control
from .dnd import dnd_button
from .dark_mode import dark_mode_button
from .ethernet import ethernet_control
from .vpn import vpn_control
from .qs_button import QSButton
from ignis.services.network import NetworkService
network = NetworkService.get_default()
def add_button(main_box: Widget.Box, buttons: tuple[QSButton, ...], i: int) -> None:
row = Widget.Box(homogeneous=True)
if len(main_box.child) > 0:
row.style = "margin-top: 0.5rem;"
main_box.append(row)
button1 = buttons[i]
row.append(button1)
if button1.content:
main_box.append(button1.content)
if i + 1 < len(buttons):
button2 = buttons[i + 1]
button2.style = "margin-left: 0.5rem;"
row.append(button2)
if button2.content:
main_box.append(button2.content)
def qs_fabric(main_box: Widget.Box, *buttons: QSButton) -> None:
for i in range(0, len(buttons), 2):
add_button(main_box, buttons, i)
def qs_config(main_box: Widget.Box) -> None:
qs_fabric(
main_box,
*wifi_control(),
*ethernet_control(),
*vpn_control(),
dnd_button(),
dark_mode_button(),
record_control(),
)
def update_box(main_box: Widget.Box):
main_box.child = []
qs_config(main_box)
def quick_settings() -> Widget.Box:
main_box = Widget.Box(vertical=True, css_classes=["qs-main-box"])
network.wifi.connect("notify::devices", lambda x, y: update_box(main_box))
network.ethernet.connect("notify::devices", lambda x, y: update_box(main_box))
network.vpn.connect("notify::connections", lambda x, y: update_box(main_box))
return main_box
@@ -0,0 +1,100 @@
from ignis.widgets import Widget
from .qs_button import QSButton
from ignis.services.recorder import RecorderService
recorder = RecorderService.get_default()
def record_control() -> QSButton:
record_audio_switch = Widget.Switch(halign="end", hexpand=True, valign="center")
dropdown = Widget.DropDown(
items=["Internal audio", "Microphone", "Both sources"],
css_classes=["record-dropdown"],
)
def start_recording(record_menu: Widget.Revealer) -> None:
record_menu.set_reveal_child(False)
microphone = False
internal = False
if record_audio_switch.active:
if dropdown.selected == "Internal audio":
internal = True
elif dropdown.selected == "Microphone":
microphone = True
else:
internal = True
microphone = True
recorder.start_recording(
record_microphone=microphone, record_internal_audio=internal
)
record_menu = Widget.Revealer(
transition_duration=300,
transition_type="slide_down",
child=Widget.Box(
css_classes=["record-menu"],
vertical=True,
child=[
Widget.Icon(
image="media-record-symbolic",
pixel_size=36,
halign="center",
css_classes=["record-icon"],
),
Widget.Label(
label="Start recording?",
halign="center",
style="font-size: 1.2rem;",
),
Widget.Box(
style="margin-top: 0.5rem;",
child=[
Widget.Icon(
image="microphone-sensitivity-medium-symbolic",
pixel_size=20,
style="margin-right: 0.5rem;",
),
Widget.Box(
vertical=True,
child=[
Widget.Label(
label="Record audio",
style="font-size: 1.1rem;",
halign="start",
),
dropdown,
],
),
record_audio_switch,
],
),
Widget.Box(
style="margin-top: 1rem;",
child=[
Widget.Button(
child=Widget.Label(label="Cancel"),
css_classes=["record-cancel-button", "unset"],
on_click=lambda x: record_menu.set_reveal_child(False), # type: ignore
),
Widget.Button(
child=Widget.Label(label="Start recording"),
halign="end",
hexpand=True,
css_classes=["record-start-button", "unset"],
on_click=lambda x: start_recording(record_menu), # type: ignore
),
],
),
],
),
)
return QSButton(
label="Recording",
icon_name="media-record-symbolic",
on_activate=lambda x: record_menu.toggle(),
on_deactivate=lambda x: recorder.stop_recording(),
active=recorder.bind("active"),
content=record_menu,
)
@@ -0,0 +1,76 @@
import os
from ignis.widgets import Widget
from ignis.utils import Utils
from ignis.app import IgnisApp
from ignis.services.fetch import FetchService
from ..settings import settings_window
from options import avatar_opt
fetch = FetchService.get_default()
app = IgnisApp.get_default()
def format_uptime(value):
days, hours, minutes, seconds = value
if days:
return f"up {days:02}:{hours:02}:{minutes:02}"
else:
return f"up {hours:02}:{minutes:02}"
def user_image() -> Widget.Picture:
return Widget.Picture(
image=avatar_opt.bind(
"value",
lambda value: "user-info" if not os.path.exists(value) else value,
),
width=44,
height=44,
content_fit="cover",
style="border-radius: 10rem;",
)
def username() -> Widget.Box:
return Widget.Box(
child=[
Widget.Label(
label=os.getenv("USER"), css_classes=["user-name"], halign="start"
),
Widget.Label(
label=Utils.Poll(
timeout=60 * 1000, callback=lambda x: fetch.uptime
).bind("output", lambda value: format_uptime(value)),
halign="start",
css_classes=["user-name-secondary"],
),
],
vertical=True,
css_classes=["user-name-box"],
)
def settings_button() -> Widget.Button:
return Widget.Button(
child=Widget.Icon(image="emblem-system-symbolic", pixel_size=20),
halign="end",
hexpand=True,
css_classes=["user-settings", "unset"],
on_click=lambda x: settings_window(),
)
def power_button() -> Widget.Button:
return Widget.Button(
child=Widget.Icon(image="system-shutdown-symbolic", pixel_size=20),
halign="end",
css_classes=["user-power", "unset"],
on_click=lambda x: app.toggle_window("ignis_POWERMENU"),
)
def user() -> Widget.Box:
return Widget.Box(
child=[user_image(), username(), settings_button(), power_button()],
css_classes=["user"],
)
@@ -0,0 +1,161 @@
from ignis.widgets import Widget
from ignis.utils import Utils
from ignis.services.audio import AudioService, Stream
audio = AudioService.get_default()
def volume_scale(stream: Stream) -> Widget.Scale:
return Widget.Scale(
css_classes=["material-slider"],
value=stream.bind("volume"),
step=5,
hexpand=True,
on_change=lambda x: stream.set_volume(x.value),
sensitive=stream.bind("is_muted", lambda value: not value),
)
def device_entry(stream: Stream, _type: str) -> Widget.Button:
widget = Widget.Button(
child=Widget.Box(
child=[
Widget.Icon(image="audio-card-symbolic"),
Widget.Label(
label=stream.description,
ellipsize="end",
max_width_chars=30,
halign="start",
css_classes=["volume-entry-label"],
),
Widget.Icon(
image="object-select-symbolic",
halign="end",
hexpand=True,
visible=stream.bind("is_default"),
),
]
),
css_classes=["volume-entry", "unset"],
hexpand=True,
on_click=lambda x: setattr(audio, _type, stream),
)
stream.connect("removed", lambda x: widget.unparent())
return widget
def device_list(
header_label: str, header_icon: str, _type: str, **kwargs
) -> Widget.Revealer:
box = Widget.Box(
css_classes=["control-center-menu"],
vertical=True,
child=[
Widget.Box(
child=[
Widget.Icon(image=header_icon, pixel_size=24),
Widget.Label(
label=header_label,
halign="start",
css_classes=["volume-entry-list-header-label"],
),
],
css_classes=["volume-entry-list-header-box"],
),
Widget.Box(
vertical=True,
setup=lambda self: audio.connect(
f"{_type}-added",
lambda x, stream: self.append(device_entry(stream, _type)),
),
),
Widget.Separator(css_classes=["volume-entry-list-separator"]),
Widget.Button(
child=Widget.Box(
child=[
Widget.Icon(image="preferences-system-symbolic"),
Widget.Label(
label="Sound Settings",
halign="start",
css_classes=["volume-entry-label"],
),
]
),
css_classes=["volume-entry", "unset"],
style="margin-bottom: 0;",
on_click=lambda x: Utils.exec_sh_async("pavucontrol"),
),
],
**kwargs,
)
return Widget.Revealer(
child=box, transition_type="slide_down", transition_duration=300
)
def volume_icon(stream: Stream) -> Widget.Button:
return Widget.Button(
child=Widget.Icon(
image=stream.bind("icon_name"),
pixel_size=18,
),
css_classes=["material-slider-icon", "unset", "hover-surface"],
on_click=lambda x: stream.set_is_muted(not stream.is_muted),
)
def device_list_arrow(device_list: Widget.Revealer) -> Widget.Button:
return Widget.ArrowButton(
arrow=Widget.Arrow(pixel_size=20),
css_classes=["material-slider-arrow", "hover-surface"],
on_click=lambda x: device_list.toggle(),
)
def volume_control():
speakers_list = device_list(
header_label="Sound Output",
header_icon="audio-headphones-symbolic",
_type="speaker",
style="margin-bottom: 1rem;",
)
microphones_list = device_list(
header_label="Sound Input",
header_icon="microphone-sensitivity-high-symbolic",
_type="microphone",
)
speaker_icon = volume_icon(audio.speaker)
microphone_icon = volume_icon(audio.microphone)
speaker_scale = volume_scale(audio.speaker)
microphone_scale = volume_scale(audio.microphone)
speaker_arrow = device_list_arrow(speakers_list)
microphone_arrow = device_list_arrow(microphones_list)
speaker_control = Widget.Box(
vertical=True,
child=[
Widget.Box(child=[speaker_icon, speaker_scale, speaker_arrow]),
speakers_list,
],
style="margin-top: 1rem;",
)
microphone_control = Widget.Box(
vertical=True,
child=[
Widget.Box(child=[microphone_icon, microphone_scale, microphone_arrow]),
microphones_list,
],
style="margin-top: 0.25rem;",
)
return Widget.Box(
vertical=True,
child=[speaker_control, microphone_control],
)
@@ -0,0 +1,120 @@
from ignis.widgets import Widget
from ignis.utils import Utils
from .qs_button import QSButton
from typing import List
from ignis.services.network import NetworkService, VpnConnection
network = NetworkService.get_default()
class VpnNetworkItem(Widget.Button):
def __init__(self, conn: VpnConnection):
super().__init__(
css_classes=["vpn-connection-item", "unset"],
on_click=lambda x: conn.toggle_connection(),
child=Widget.Box(
child=[
Widget.Label(
label=conn.name,
ellipsize="end",
max_width_chars=20,
halign="start",
css_classes=["vpn-connection-label"],
),
Widget.Button(
child=Widget.Label(
label=conn.bind(
"is_connected",
lambda value: "Disconnect" if value else "Connect",
)
),
css_classes=["vpn-connection-item-connect-label", "unset"],
halign="end",
hexpand=True,
),
]
),
)
def vpn_qsbutton() -> QSButton:
networks_list = Widget.Revealer(
transition_duration=300,
transition_type="slide_down",
child=Widget.Box(
vertical=True,
css_classes=["control-center-menu"],
child=[
Widget.Box(
css_classes=["vpn-header-box"],
child=[
Widget.Icon(
icon_name="network-vpn-symbolic", pixel_size=28),
Widget.Label(
label="VPN connections",
css_classes=["vpn-header-label"],
),
],
),
Widget.Box(
vertical=True,
child=network.vpn.bind(
"connections",
transform=lambda value: [
VpnNetworkItem(i) for i in value],
),
),
Widget.Separator(
css_classes=["vpn-connection-list-separator"]),
Widget.Button(
css_classes=["vpn-connection-item", "unset"],
on_click=lambda x: Utils.exec_sh_async(
"nm-connection-editor"),
style="margin-bottom: 0;",
child=Widget.Box(
child=[
Widget.Icon(image="preferences-system-symbolic"),
Widget.Label(
label="Network Manager",
halign="start",
css_classes=["vpn-connection-label"],
),
]
),
),
],
),
)
def get_label(id: str) -> str:
if id:
return id
else:
return "VPN"
def get_icon(icon_name: str) -> str:
if network.vpn.is_connected:
return icon_name
else:
return "network-vpn-symbolic"
def toggle_list(x) -> None:
networks_list.toggle()
return QSButton(
label=network.vpn.bind("active_vpn_id", get_label),
icon_name=network.vpn.bind("icon-name", get_icon),
on_activate=toggle_list,
on_deactivate=toggle_list,
active=network.vpn.bind("is-connected"),
content=networks_list,
)
def vpn_control() -> List[QSButton]:
if len(network.vpn.connections) > 0:
return [vpn_qsbutton()]
else:
return []
@@ -0,0 +1,112 @@
from ignis.widgets import Widget
from ignis.utils import Utils
from .qs_button import QSButton
from ignis.services.network import NetworkService, WifiAccessPoint, WifiDevice
from typing import List
network = NetworkService.get_default()
class WifiNetworkItem(Widget.Button):
def __init__(self, access_point: WifiAccessPoint):
super().__init__(
css_classes=["wifi-network-item", "unset"],
on_click=lambda x: access_point.connect_to_graphical(),
child=Widget.Box(
child=[
Widget.Icon(
image=access_point.bind(
"strength", transform=lambda value: access_point.icon_name
),
),
Widget.Label(
label=access_point.ssid,
halign="start",
css_classes=["wifi-network-label"],
),
Widget.Icon(
image="object-select-symbolic",
halign="end",
hexpand=True,
visible=access_point.bind("is_connected"),
),
]
),
)
def wifi_qsbutton(device: WifiDevice) -> QSButton:
networks_list = Widget.Revealer(
transition_duration=300,
transition_type="slide_down",
child=Widget.Box(
vertical=True,
css_classes=["control-center-menu"],
child=[
Widget.Box(
child=[
Widget.Label(label="Wi-Fi", css_classes=["wifi-header-label"]),
Widget.Switch(
halign="end",
hexpand=True,
active=network.wifi.enabled,
on_change=lambda x, state: network.wifi.set_enabled(state),
),
],
css_classes=["toggle-box", "wifi-header-box"],
),
Widget.Box(
vertical=True,
child=device.bind(
"access_points",
transform=lambda value: [WifiNetworkItem(i) for i in value],
),
),
Widget.Separator(css_classes=["wifi-network-list-separator"]),
Widget.Button(
css_classes=["wifi-network-item", "unset"],
on_click=lambda x: Utils.exec_sh_async("nm-connection-editor"),
style="margin-bottom: 0;",
child=Widget.Box(
child=[
Widget.Icon(image="preferences-system-symbolic"),
Widget.Label(
label="Network Settings",
halign="start",
css_classes=["wifi-network-label"],
),
]
),
),
],
),
)
def get_label(ssid: str) -> str:
if ssid:
return ssid
else:
return "Wi-Fi"
def get_icon(icon_name: str) -> str:
if device.ap.is_connected:
return icon_name
else:
return "network-wireless-symbolic"
def toggle_list(x) -> None:
device.scan()
networks_list.toggle()
return QSButton(
label=device.ap.bind("ssid", get_label),
icon_name=device.ap.bind("icon-name", get_icon),
on_activate=toggle_list,
on_deactivate=toggle_list,
active=network.wifi.bind("enabled"),
content=networks_list,
)
def wifi_control() -> List[QSButton]:
return [wifi_qsbutton(dev) for dev in network.wifi.devices]
@@ -0,0 +1,212 @@
import re
from ignis.widgets import Widget
from ignis.app import IgnisApp
from ignis.services.applications import (
ApplicationsService,
Application,
ApplicationAction,
)
from ignis.utils import Utils
from gi.repository import Gio # type: ignore
app = IgnisApp.get_default()
applications = ApplicationsService.get_default()
def is_url(url: str) -> bool:
regex = re.compile(
r"^(?:http|ftp)s?://" # http:// or https://
r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|" # domain
r"localhost|" # localhost
r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|" # or ipv4
r"\[?[A-F0-9]*:[A-F0-9:]+\]?)" # or ipv6
r"(?::\d+)?" # optional port
r"(?:/?|[/?]\S+)$",
re.IGNORECASE,
)
return re.match(regex, url) is not None
class LauncherAppItem(Widget.Button):
def __init__(self, application: Application) -> None:
self._application = application
super().__init__(
on_click=lambda x: self.launch(),
on_right_click=lambda x: self._menu.popup(),
css_classes=["launcher-app"],
child=Widget.Box(
child=[
Widget.Icon(image=application.icon, pixel_size=48),
Widget.Label(
label=application.name,
ellipsize="end",
max_width_chars=30,
css_classes=["launcher-app-label"],
),
]
),
)
self.__sync_menu()
application.connect("notify::is-pinned", lambda x, y: self.__sync_menu())
def launch(self) -> None:
self._application.launch()
app.close_window("ignis_LAUNCHER")
def launch_action(self, action: ApplicationAction) -> None:
action.launch()
app.close_window("ignis_LAUNCHER")
def __sync_menu(self) -> None:
self._menu = Widget.PopoverMenu(
items=[
Widget.MenuItem(label="Launch", on_activate=lambda x: self.launch()),
Widget.Separator(),
]
+ [
Widget.MenuItem(
label=i.name,
on_activate=lambda x, action=i: self.launch_action(action),
)
for i in self._application.actions
]
+ [
Widget.Separator(),
Widget.MenuItem(
label="Pin", on_activate=lambda x: self._application.pin()
)
if not self._application.is_pinned
else Widget.MenuItem(
label="Unpin", on_activate=lambda x: self._application.unpin()
),
]
)
self.child.append(self._menu)
class SearchWebButton(Widget.Button):
def __init__(self, query: str):
self._query = query
self._url = ""
browser_desktop_file = Utils.exec_sh(
"xdg-settings get default-web-browser"
).stdout.replace("\n", "")
app_info = Gio.DesktopAppInfo.new(desktop_id=browser_desktop_file)
icon_name = "applications-internet-symbolic"
if app_info:
icon_string = app_info.get_string("Icon")
if icon_string:
icon_name = icon_string
if not query.startswith(("http://", "https://")) and "." in query:
query = "https://" + query
if is_url(query):
label = f"Visit {query}"
self._url = query
else:
label = "Search in Google"
self._url = f"https://www.google.com/search?q={query.replace(' ', '+')}"
super().__init__(
on_click=lambda x: self.launch(),
css_classes=["launcher-app"],
child=Widget.Box(
child=[
Widget.Icon(image=icon_name, pixel_size=48),
Widget.Label(
label=label,
css_classes=["launcher-app-label"],
),
]
),
)
def launch(self) -> None:
Utils.exec_sh_async(f"xdg-open {self._url}")
app.close_window("ignis_LAUNCHER")
def launcher() -> Widget.Window:
def search(entry: Widget.Entry, app_list: Widget.Box) -> None:
query = entry.text
if query == "":
entry.grab_focus()
app_list.visible = False
return
apps = applications.search(applications.apps, query)
if apps == []:
app_list.child = [SearchWebButton(query)]
else:
app_list.visible = True
app_list.child = [LauncherAppItem(i) for i in apps[:5]]
def on_open(window: Widget.Window, entry: Widget.Entry) -> None:
if not window.visible:
return
entry.text = ""
entry.grab_focus()
app_list = Widget.Box(vertical=True, visible=False, style="margin-top: 1rem;")
entry = Widget.Entry(
hexpand=True,
placeholder_text="Search",
css_classes=["launcher-search"],
on_change=lambda x: search(x, app_list),
on_accept=lambda x: app_list.child[0].launch()
if len(app_list.child) > 0
else None,
)
main_box = Widget.Box(
vertical=True,
valign="start",
halign="center",
css_classes=["launcher"],
child=[
Widget.Box(
css_classes=["launcher-search-box"],
child=[
Widget.Icon(
icon_name="system-search-symbolic",
pixel_size=24,
style="margin-right: 0.5rem;",
),
entry,
],
),
app_list,
],
)
return Widget.Window(
namespace="ignis_LAUNCHER",
visible=False,
popup=True,
kb_mode="on_demand",
# exclusivity="ignore",
css_classes=["unset"],
setup=lambda self: self.connect(
"notify::visible", lambda x, y: on_open(self, entry)
),
anchor=["top", "right", "bottom", "left"],
child=Widget.Overlay(
child=Widget.Button(
vexpand=True,
hexpand=True,
can_focus=False,
css_classes=["unset"],
on_click=lambda x: app.close_window("ignis_LAUNCHER"),
style="background-color: rgba(0, 0, 0, 0.3);",
),
overlays=[main_box],
),
)
@@ -0,0 +1,95 @@
from ignis.widgets import Widget
from ignis.app import IgnisApp
from ignis.utils import Utils
from ignis.services.notifications import Notification, NotificationService
from ..control_center.notification_center import NotificationWidget
app = IgnisApp.get_default()
notifications = NotificationService.get_default()
class Popup(Widget.Box):
def __init__(self, notification: Notification):
widget = NotificationWidget(notification)
widget.css_classes = ["notification-popup"]
self._inner = Widget.Revealer(transition_type="slide_left", child=widget)
self._outer = Widget.Revealer(transition_type="slide_down", child=self._inner)
super().__init__(child=[self._outer], halign="end")
notification.connect("dismissed", lambda x: self.destroy())
def destroy(self):
def box_destroy():
box: Widget.Box = self.get_parent() # type: ignore
if not box:
return
self.unparent()
if len(notifications.popups) == 0:
window: Widget.Window = box.get_parent() # type: ignore
if not window:
return
window.visible = False
else:
change_window_input_region(box)
def outer_close():
self._outer.reveal_child = False
Utils.Timeout(self._outer.transition_duration, box_destroy)
self._inner.transition_type = "crossfade"
self._inner.reveal_child = False
Utils.Timeout(self._outer.transition_duration, outer_close)
def on_notified(box: Widget.Box, notification: Notification, monitor: int) -> None:
window: Widget.Window = app.get_window("ignis_CONTROL_CENTER") # type: ignore
if window.visible and window.monitor == monitor:
return
app.open_window(f"ignis_NOTIFICATION_POPUP_{monitor}")
popup = Popup(notification)
box.prepend(popup)
popup._outer.reveal_child = True
Utils.Timeout(popup._outer.transition_duration, reveal_popup, box, popup)
def reveal_popup(box: Widget.Box, popup: Popup) -> None:
popup._inner.set_reveal_child(True)
change_window_input_region(box)
def change_window_input_region(box: Widget.Box) -> None:
def callback() -> None:
width = box.get_width()
height = box.get_height()
window: Widget.Window = box.get_parent() # type: ignore
window.input_width = width
window.input_height = height
Utils.Timeout(ms=50, target=callback)
def notification_popup(monitor: int) -> Widget.Window:
notifications_box = Widget.Box(
vertical=True,
valign="start",
setup=lambda self: notifications.connect(
"new_popup",
lambda x, notification: on_notified(self, notification, monitor),
),
)
return Widget.Window(
anchor=["right", "top", "bottom"],
monitor=monitor,
namespace=f"ignis_NOTIFICATION_POPUP_{monitor}",
layer="top",
child=notifications_box,
visible=False,
css_classes=["rec-unset"],
style="min-width: 29rem;",
)
@@ -0,0 +1,43 @@
from ignis.widgets import Widget
from ignis.utils import Utils
from ignis.services.audio import AudioService
audio = AudioService.get_default()
class OSD(Widget.Window):
def __init__(self):
self._timer = None
super().__init__(
layer="overlay",
anchor=["bottom"],
namespace="ignis_OSD",
visible=False,
css_classes=["rec-unset"],
child=Widget.Box(
css_classes=["osd"],
child=[
Widget.Icon(
pixel_size=26,
style="margin-right: 0.5rem;",
image=audio.speaker.bind("icon_name"),
),
Widget.Scale(
value=audio.speaker.bind("volume"),
css_classes=["material-slider"],
sensitive=False,
hexpand=True,
),
],
),
)
def set_property(self, property_name, value):
if property_name == "visible":
if self._timer:
self._timer.cancel()
self._timer = None
self._timer = Utils.Timeout(3000, self.set_visible, False)
super().set_property(property_name, value)
@@ -0,0 +1,98 @@
from ignis.widgets import Widget
from ignis.utils import Utils
from ignis.app import IgnisApp
from typing import Callable
app = IgnisApp.get_default()
class PowermenuButton(Widget.Box):
def __init__(self, label: str, icon_name: str, on_click: Callable) -> None:
super().__init__(
child=[
Widget.Button(
child=Widget.Icon(image=icon_name, pixel_size=36),
on_click=on_click,
css_classes=["powermenu-button", "unset"],
),
Widget.Label(label=label, css_classes=["powermenu-button-label"]),
],
vertical=True,
css_classes=["powermenu-button-box"],
)
def poweroff(*args) -> None:
Utils.exec_sh_async("poweroff")
def reboot(*args) -> None:
Utils.exec_sh_async("reboot")
def suspend(*args) -> None:
app.close_window("ignis_POWERMENU")
Utils.exec_sh_async("systemctl suspend && hyprlock")
def hypr_exit(*args) -> None:
Utils.exec_sh_async("hyprctl dispatch exit 0")
def powermenu():
return Widget.Window(
popup=True,
kb_mode="on_demand",
namespace="ignis_POWERMENU",
exclusivity="ignore",
anchor=["left", "right", "top", "bottom"],
visible=False,
child=Widget.Overlay(
child=Widget.Button(
vexpand=True,
hexpand=True,
can_focus=False,
css_classes=["unset", "powermenu-overlay"],
on_click=lambda x: app.close_window("ignis_POWERMENU"),
),
overlays=[
Widget.Box(
vertical=True,
valign="center",
halign="center",
css_classes=["powermenu"],
child=[
Widget.Box(
child=[
PowermenuButton(
label="Power off",
icon_name="system-shutdown-symbolic",
on_click=poweroff,
),
PowermenuButton(
label="Reboot",
icon_name="system-reboot-symbolic",
on_click=reboot,
),
]
),
Widget.Box(
child=[
PowermenuButton(
label="Suspend",
icon_name="night-light-symbolic",
on_click=suspend,
),
PowermenuButton(
label="Sign out",
icon_name="system-log-out-symbolic",
on_click=hypr_exit,
),
]
),
],
)
],
),
css_classes=["unset"],
)
@@ -0,0 +1,83 @@
from ignis.widgets import Widget
from ignis.gobject import IgnisGObject
from gi.repository import GObject # type: ignore
from .notifications import notifications_entry
from .about import about_entry
from .appearance import appearance_entry
from .recorder import recorder_entry
from .user import user_entry
from .elements import SettingsPage
from ignis.app import IgnisApp
from ignis.exceptions import WindowNotFoundError
from options import settings_last_page
app = IgnisApp.get_default()
class ActivePage(IgnisGObject):
def __init__(self, name: str | None, page: SettingsPage | None):
super().__init__()
self._name = name
self._page = page
@GObject.Property
def name(self) -> str | None:
return self._name
@name.setter
def name(self, value: str | None) -> None:
self._name = value
@GObject.Property
def page(self) -> SettingsPage | None:
return self._page
@page.setter
def page(self, value: SettingsPage | None) -> None:
self._page = value
def settings_widget():
active_page = ActivePage(name="Settings", page=None)
content = Widget.Box(
hexpand=True,
vexpand=True,
child=active_page.bind("page", transform=lambda value: [value]),
)
listbox = Widget.ListBox(
rows=[
notifications_entry(active_page),
recorder_entry(active_page),
appearance_entry(active_page),
user_entry(active_page),
about_entry(active_page),
],
)
listbox.select_row(listbox.rows[settings_last_page.value])
navigation_sidebar = Widget.Box(
vertical=True,
css_classes=["settings-sidebar"],
child=[
Widget.Label(
label="Settings", halign="start", css_classes=["settings-sidebar-label"]
),
listbox,
],
)
return Widget.Box(child=[navigation_sidebar, content])
def settings_window():
try:
app.get_window("ignis_SETTINGS")
except WindowNotFoundError:
return Widget.RegularWindow(
default_width=900,
default_height=600,
resizable=False,
child=settings_widget(),
namespace="ignis_SETTINGS",
)
@@ -0,0 +1,44 @@
from .elements import SettingsPage, SettingsRow, SettingsEntry
from ignis.utils import Utils
from ignis.widgets import Widget
from services.material import MaterialService
from ignis.services.fetch import FetchService
fetch = FetchService.get_default()
material = MaterialService.get_default()
def about_entry(active_page):
about_page = SettingsPage(
name="About",
groups=[
Widget.Box(
child=[
Widget.Picture(
image=material.bind(
"dark_mode",
transform=lambda value: fetch.os_logo_text_dark
if value
else fetch.os_logo_text,
),
width=300,
height=100,
)
],
halign="center",
width_request=300,
height_request=100,
),
SettingsRow(label="OS", sublabel=fetch.os_name),
SettingsRow(label="Ignis version", sublabel=Utils.get_ignis_version()),
SettingsRow(label="Session type", sublabel=fetch.session_type),
SettingsRow(label="Wayland compositor", sublabel=fetch.current_desktop),
SettingsRow(label="Kernel", sublabel=fetch.kernel),
],
)
return SettingsEntry(
label="About",
icon="help-about-symbolic",
active_page=active_page,
page=about_page,
)
@@ -0,0 +1,64 @@
import os
from services.material import MaterialService
from .elements import SwitchRow, SettingsPage, SettingsGroup, FileRow, SettingsEntry
from ignis.widgets import Widget
from ignis.services.wallpaper import WallpaperService
wallpaper = WallpaperService.get_default()
material = MaterialService.get_default()
def appearance_entry(active_page):
appearance_page = SettingsPage(
name="Appearance",
groups=[
SettingsGroup(
name=None,
rows=[
Widget.ListBoxRow(
child=Widget.Picture(
image=wallpaper.bind("wallpaper"),
width=1920 // 4,
height=1080 // 4,
halign="center",
style="border-radius: 1rem;",
content_fit="cover",
),
selectable=False,
activatable=False,
),
SwitchRow(
label="Dark mode",
active=material.bind("dark_mode"),
on_change=lambda x, state: material.set_dark_mode(state),
style="margin-top: 1rem;",
),
FileRow(
label="Wallpaper path",
button_label=os.path.basename(wallpaper.wallpaper)
if wallpaper.wallpaper
else None,
dialog=Widget.FileDialog(
on_file_set=lambda x, file: material.generate_colors(
file.get_path()
),
initial_path=wallpaper.bind("wallpaper"),
filters=[
Widget.FileFilter(
mime_types=["image/jpeg", "image/png"],
default=True,
name="Images JPEG/PNG",
)
],
),
),
],
)
],
)
return SettingsEntry(
label="Appearance",
icon="preferences-desktop-wallpaper-symbolic",
active_page=active_page,
page=appearance_page,
)
@@ -0,0 +1,20 @@
from .row import SettingsRow
from .page import SettingsPage
from .group import SettingsGroup
from .switchrow import SwitchRow
from .filerow import FileRow
from .spinrow import SpinRow
from .entryrow import EntryRow
from .entry import SettingsEntry
__all__ = [
"SettingsRow",
"SettingsPage",
"SettingsGroup",
"SettingsRow",
"SwitchRow",
"FileRow",
"SpinRow",
"EntryRow",
"SettingsEntry",
]

Some files were not shown because too many files have changed in this diff Show More