Fixes GTK4 Layer Shell and Ollama integration issues

Addresses multiple issues related to GTK4 Layer Shell initialization and Ollama integration.

- Reorders initialization to ensure the layer shell is set up before window properties.
- Adds error detection for layer shell initialization failures.
- Implements a focus event handler to prevent focus-out warnings.
- Introduces a launcher script to activate the virtual environment, force native Wayland, and preload the GTK4 Layer Shell library.
- Warns users of incorrect GDK_BACKEND settings.
- Updates the Ollama client to handle responses from both older and newer versions of the Ollama SDK.

These changes improve the application's stability, compatibility, and functionality on Wayland systems.
This commit is contained in:
Melvin Ragusa
2025-10-25 21:23:32 +02:00
parent da57c43e69
commit 1a80358ffc
10 changed files with 638 additions and 8 deletions

View File

@@ -0,0 +1,5 @@
from .quickcenter import QuickCenter
__all__ = [
"QuickCenter",
]

View File

@@ -0,0 +1,150 @@
from ignis import widgets
from ignis.window_manager import WindowManager
from ignis.services.notifications import NotificationService
from modules.m3components import Button
from .widgets import NotificationCenter, QuickSliders
from user_settings import user_settings
from ignis.services.niri import NiriService
window_manager = WindowManager.get_default()
notifications = NotificationService.get_default()
class QuickCenter(widgets.RevealerWindow):
def open_window(self, window):
window_manager.close_window("QuickCenter")
window_manager.open_window(window)
def __init__(self):
notification_center = NotificationCenter()
quick_sliders = QuickSliders()
bottom_controls = widgets.Box(
css_classes=["bottom-controls"],
hexpand=True,
halign="fill",
homogeneous=False,
spacing=5,
child=[
Button.button(
icon="power_settings_new",
halign="start",
hexpand=False,
on_click=lambda x: self.open_window("PowerMenu"),
vexpand=False,
valign="center",
size="xs",
),
Button.button(
icon="settings",
halign="start",
hexpand=False,
on_click=lambda x: self.open_window("Settings"),
vexpand=False,
valign="center",
size="xs",
),
Button.button(
icon="clear_all",
label="Clear all",
halign="end",
hexpand=True,
on_click=lambda x: notifications.clear_all(),
css_classes=["notification-clear-all"],
vexpand=True,
valign="center",
size="xs",
visible=notifications.bind(
"notifications", lambda value: len(value) != 0
),
),
],
)
self.content_box = widgets.Box(
vertical=True,
spacing=0,
hexpand=False,
css_classes=["quick-center"],
child=[notification_center, quick_sliders, bottom_controls],
)
self.content_box.width_request = 400
revealer = widgets.Revealer(
child=self.content_box,
transition_duration=300,
)
close_button = widgets.Button(
vexpand=True,
hexpand=True,
can_focus=False,
on_click=lambda x: window_manager.close_window("QuickCenter"),
)
main_overlay = widgets.Overlay(
css_classes=["popup-close"],
child=close_button,
overlays=[revealer],
)
super().__init__(
revealer=revealer,
child=main_overlay,
css_classes=["popup-close"],
hide_on_close=True,
visible=False,
namespace="QuickCenter",
popup=True,
layer="overlay",
kb_mode="exclusive",
anchor=["left", "right", "top", "bottom"],
)
self.window_manager = window_manager
self.notification_center = notification_center
self.revealer = revealer
self.actual_content_box = revealer
self.niri = NiriService.get_default()
self.connect("notify::visible", self._toggle_revealer)
self.update_side()
def _toggle_revealer(self, *_):
self.revealer.reveal_child = self.visible
def update_side(self):
position = user_settings.interface.modules
location = position.location.systeminfotray
bar = (
user_settings.interface.bar
if position.bar_id.systeminfotray == 0
else user_settings.interface.bar
)
side = bar.side
if side in ["left", "right"]:
self.actual_content_box.set_halign("start" if side == "left" else "end")
self.actual_content_box.anchor = ["top", "bottom", side]
else:
if location == "center":
self.actual_content_box.set_halign("center")
self.actual_content_box.anchor = ["top", "bottom"]
else:
self.actual_content_box.set_halign("start" if location == 0 else "end")
self.actual_content_box.anchor = [
"top",
"bottom",
"left" if location == 0 else "end",
]
self.revealer.transition_type = "none"
if self.niri and self.niri.is_available:
self.revealer.transition_type = (
"slide_right"
if self.actual_content_box.halign == "start"
else "slide_left"
)
self.content_box.set_halign(
"end" if self.actual_content_box.halign == "start" else "end"
)
self.actual_content_box.queue_resize()

View File

@@ -0,0 +1,7 @@
from .notificationcenter import NotificationCenter
from .sliders import QuickSliders
__all__ = [
"NotificationCenter",
"QuickSliders"
]

View File

@@ -0,0 +1,98 @@
from ignis import widgets, utils
from ignis.services.notifications import Notification, NotificationService
from ignis.window_manager import WindowManager
from gi.repository import GLib, Gtk
from ...notifications import ExoNotification
notifications = NotificationService.get_default()
window_manager = WindowManager.get_default()
class Popup(widgets.Revealer):
def __init__(self, notification: Notification, **kwargs):
widget = ExoNotification(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)
class Notifications(widgets.Box):
def __init__(self):
loading_notifications_label = widgets.Label(
label="Loading notifications...",
valign="center",
vexpand=True,
css_classes=["notification-center-info-label"],
)
super().__init__(
vertical=True,
child=[loading_notifications_label],
vexpand=True,
css_classes=["notification-center-content"],
spacing=2,
setup=lambda self: notifications.connect(
"notified",
lambda x, notification: self.__on_notified(notification),
),
)
utils.ThreadTask(
self.__load_notifications,
lambda result: self.set_child(result),
).run()
def __on_notified(self, notification: Notification) -> None:
notify = Popup(notification)
self.prepend(notify)
notify.reveal_child = True
def __load_notifications(self) -> list[widgets.Label | Popup]:
contents: list[widgets.Label | Popup] = []
for i in reversed(notifications.notifications):
GLib.idle_add(lambda i=i: contents.append(Popup(i, reveal_child=True)))
contents.append(
widgets.Label(
label="notifications_off",
valign="end",
vexpand=True,
css_classes=["notification-center-info-icon"],
visible=notifications.bind(
"notifications", lambda value: len(value) == 0
),
)
)
contents.append(
widgets.Label(
label="No notifications",
valign="start",
vexpand=True,
css_classes=["notification-center-info-label"],
visible=notifications.bind(
"notifications", lambda value: len(value) == 0
),
)
)
return contents
class NotificationCenter(widgets.Box):
__gtype_name__ = "NotificationCenter"
def __init__(self):
scroll = widgets.Scroll(child=Notifications(), vexpand=True)
scroll.set_overflow(Gtk.Overflow.HIDDEN)
super().__init__(
vertical=True,
vexpand=True,
css_classes=["notification-center"],
spacing=10,
child=[
scroll,
],
)

View File

@@ -0,0 +1,94 @@
import asyncio
from gi.repository import GLib
from ignis import widgets
from ignis.services.audio import AudioService
from ignis.services.backlight import BacklightService
from ignis.window_manager import WindowManager
audio = AudioService.get_default()
backlight = BacklightService.get_default()
window_manager = WindowManager.get_default()
class QuickSliders(widgets.Box):
def __init__(self):
children = []
if audio.speaker:
self.volume_slider = widgets.Scale(
min=0,
max=100,
step=1.0,
on_change=self.on_volume_changed,
hexpand=True,
)
volume_box = widgets.Box(
css_classes=["m3-slider"],
child=[
widgets.Label(label="volume_up", css_classes=["m3-icon"]),
self.volume_slider,
],
spacing=12,
)
children.append(volume_box)
if backlight.available:
self.backlight_slider = widgets.Scale(
min=0,
max=100,
step=1.0,
on_change=self.on_backlight_changed,
hexpand=True,
)
backlight_box = widgets.Box(
css_classes=["m3-slider"],
child=[
widgets.Label(label="brightness_6", css_classes=["m3-icon"]),
self.backlight_slider,
],
spacing=12,
)
children.append(backlight_box)
super().__init__(
css_classes=["quick-sliders-container"],
hexpand=True,
halign="fill",
spacing=2,
vertical=True,
child=children,
)
if audio.speaker:
audio.speaker.connect("notify::volume", self._on_volume_changed)
audio.speaker.connect("notify::is-muted", self._on_volume_changed)
if backlight.available:
backlight.connect("notify::brightness", self._on_brightness_changed)
def _on_volume_changed(self, stream, *_):
if stream.is_muted:
self.volume_slider.set_value(0)
else:
self.volume_slider.set_value(stream.volume)
def _on_brightness_changed(self, backlight, *_):
self.backlight_slider.set_value(
(backlight.brightness / backlight.max_brightness) * 100
)
def on_volume_changed(self, slider):
value = slider.get_value()
self.set_suppress_osd_flag()
audio.speaker.volume = value
def on_backlight_changed(self, slider):
value = slider.get_value()
self.set_suppress_osd_flag()
backlight.brightness = int((value / 100) * backlight.max_brightness)
def set_suppress_osd_flag(self):
window_manager.suppress_osd = True
asyncio.create_task(self.reset_suppress_osd_flag())
async def reset_suppress_osd_flag(self):
await asyncio.sleep(0.1)
window_manager.suppress_osd = False