✨ feat(dashboard): implement dashboard with notifications and keybinding

This commit is contained in:
sankalp committed 2025-07-27 01:23:49 +05:45
1 parent c4be327948
commit 8f0891220e
9 files changed
+478 -126

No files matched your search

+2
View File
@@ -37,6 +37,8 @@ DEFAULTS = {
"suffix_restart_inspector": "B",
"prefix_settings": "SUPER",
"suffix_settings": "I",
"prefix_dashboard": "SUPER",
"suffix_dashboard": "G",
"wallpapers_dir": "/home/sankalp/Pictures/wallpapers",
"vertical": True,
"dock_enabled": True,
+1
View File
@@ -68,6 +68,7 @@ class KeyBindingsTab:
("Kanban", "prefix_kanban", "suffix_kanban"),
("Power Menu", "prefix_power", "suffix_power"),
("Toggle Caffeine", "prefix_caffeine", "suffix_caffeine"),
("Dashboard", "prefix_dashboard", "suffix_dashboard"),
("Settings", "prefix_settings", "suffix_settings"),
(
"Restart with inspector",
+2
View File
@@ -282,6 +282,8 @@ bind = {bind_vars.get("prefix_power", "SUPER")}, {bind_vars.get("suffix_power",
bind = {bind_vars.get("prefix_caffeine", "SUPER SHIFT")}, {bind_vars.get("suffix_caffeine", "M")}, exec, $fabricSend "launcher.show_launcher('caffeine on', external=True)"
# Settings
bind = {bind_vars.get("prefix_settings", "SUPER")}, {bind_vars.get("suffix_settings", "I")}, exec, uwsm-app $(python {home}/.config/{APP_NAME_CAP}/config/config.py) # Settings
# Dashboard
bind = {bind_vars.get("prefix_dashboard", "SUPER")}, {bind_vars.get("suffix_dashboard", "G")}, exec, $fabricSend "dashboard.toggle_dashboard()"
# Restart Modus
bind = {bind_vars.get("prefix_restart_inspector", "SUPER CTRL ALT")}, {bind_vars.get("suffix_restart_inspector", "B")}, exec, killall {APP_NAME}; uwsm-app $(GTK_DEBUG=interactive python {home}/.config/{APP_NAME_CAP}/main.py) # Restart with inspector
+2 -28
View File
@@ -69,12 +69,8 @@ tooltip > * {
box-shadow: 0 0 3px alpha(black, 0.7);
}
#dashboard.hide {
margin: -999px;
transition: margin 0.25s cubic-bezier(0.5, 0.25, 0, 1);
}
#dashboard.reveal {
margin: 0px;
#dashboard-window {
transition: margin 0.25s cubic-bezier(0.5, 0.25, 0, 1);
}
#dashboard {
@@ -154,26 +150,4 @@ tooltip > * {
border-color: var(--primary);
}
#notification-source {
color: var(--outline);
}
#notificationss-icon {
font-size: 36px;
margin-right: 27px;
margin-left: 7px;
color: var(--surface-bright);
}
#notification-container {
background-color: var(--surface);
border-radius: 20px;
}
#notification-box {
background-color: black;
border-radius: 25px;
padding: 10px;
min-width: 364px; /* 290+74 */
margin-top: 3px;
}
+1 -1
View File
@@ -12,7 +12,7 @@ from modules.launcher import Launcher
from modules.notification_popup import NotificationPopup
from modules.osd import OSD
from modules.switcher import ApplicationSwitcher
from modules.dashboard.main import DashboardWindow
from modules.dashboard import DashboardWindow
for log in [
+8
View File
@@ -0,0 +1,8 @@
"""
Dashboard module for Modus.
Provides a dashboard interface with tiles and notifications.
"""
from .main import DashboardWindow
__all__ = ["DashboardWindow"]
+415 -94
View File
@@ -1,19 +1,384 @@
from fabric.widgets.box import Box
from fabric.widgets.label import Label
from fabric.widgets.centerbox import CenterBox
from fabric.widgets.button import Button
from fabric.widgets.revealer import Revealer
from fabric.widgets.scrolledwindow import ScrolledWindow
from fabric.widgets.centerbox import CenterBox
from fabric.core.service import Property
from gi.repository import GLib, Gtk
import os
import json
from datetime import datetime
from modules.dashboard.bluetooth import Bluetooth
from modules.dashboard.network import Network
from modules.dashboard.tile import Tile, TileSpecial
from utils.wavy_clock import WavyCircle
from modules.dashboard.tile import TileSpecial
from modules.dashboard.player_mini import PlayerContainerMini
from utils.notification_utils import (
get_shared_notification_history,
PERSISTENT_HISTORY_FILE,
create_historical_notification_from_data,
compute_time_label,
)
from utils.wayland import WaylandWindow as Window
import utils.icons as icons
import config.info as info
class DashboardNotificationItem(Box):
"""Individual notification item for dashboard"""
def __init__(self, notification_data, **kwargs):
super().__init__(
name="notification-container",
orientation="v",
h_align="fill",
h_expand=True,
**kwargs
)
self.notification_data = notification_data
# Create historical notification from data
hist_notif = create_historical_notification_from_data(notification_data)
# Create timestamp
try:
arrival_time = datetime.fromisoformat(hist_notif.timestamp)
except Exception:
arrival_time = datetime.now()
# Create time label
self.time_label = Label(
name="notification-timestamp",
markup=compute_time_label(arrival_time),
h_align="start",
ellipsization="end",
)
# Create notification image box
# For dashboard, we'll use a simple icon instead of trying to load cached images
self.notif_image_box = Box(
name="notification-image",
orientation="v",
children=[
Label(name="notification-icon", markup=icons.notifications),
Box(v_expand=True),
],
)
# Create summary label
self.notif_summary_label = Label(
name="notification-summary",
markup=hist_notif.summary,
h_align="start",
ellipsization="end",
)
# Create app name label
self.notif_app_name_label = Label(
name="notification-app-name",
markup=f"{hist_notif.app_name}",
h_align="start",
ellipsization="end",
)
# Create body label
self.notif_body_label = (
Label(
name="notification-body",
markup=hist_notif.body,
h_align="start",
ellipsization="end",
line_wrap="word-char",
)
if hist_notif.body
else Box()
)
if hist_notif.body:
self.notif_body_label.set_single_line_mode(True)
# Create summary box with separators
self.notif_summary_box = Box(
name="notification-summary-box",
orientation="h",
children=[
self.notif_summary_label,
Box(
name="notif-sep",
h_expand=False,
v_expand=False,
h_align="center",
v_align="center",
),
self.notif_app_name_label,
Box(
name="notif-sep",
h_expand=False,
v_expand=False,
h_align="center",
v_align="center",
),
self.time_label,
],
)
# Create text box
self.notif_text_box = Box(
name="notification-text",
orientation="v",
v_align="center",
h_expand=True,
children=[
self.notif_summary_box,
self.notif_body_label,
],
)
# Create close button
self.notif_close_button = Button(
name="notif-close-button",
child=Label(name="notif-close-label", markup=icons.cancel),
on_clicked=lambda *_: self.remove_notification(),
)
self.notif_close_button_box = Box(
orientation="v",
children=[
self.notif_close_button,
Box(v_expand=True),
],
)
# Create main content box
content_box = Box(
name="notification-box-hist",
spacing=8,
children=[
self.notif_image_box,
self.notif_text_box,
self.notif_close_button_box,
],
)
self.add(content_box)
def remove_notification(self):
"""Remove this notification from the dashboard"""
if self.get_parent():
self.get_parent().remove(self)
self.destroy()
class DashboardNotifications(Box):
"""Dashboard notifications container that shows real notifications"""
def __init__(self, **kwargs):
super().__init__(
name="notification-history",
orientation="v",
**kwargs
)
self.notification_items = []
self.max_notifications = 5 # Limit to 5 notifications in dashboard
# Create header components (matching dock notifications)
self.header_label = Label(
name="nhh",
label="Notifications",
h_align="start",
h_expand=True,
)
self.header_switch = Gtk.Switch(name="dnd-switch")
self.header_switch.set_vexpand(False)
self.header_switch.set_valign(Gtk.Align.CENTER)
self.header_switch.set_active(False)
self.header_clean = Button(
name="nhh-button",
child=Label(name="nhh-button-label", markup=icons.trash),
on_clicked=self.clear_history,
)
self.do_not_disturb_enabled = False
self.header_switch.connect("notify::active", self.on_do_not_disturb_changed)
self.dnd_label = Label(name="dnd-label", markup=icons.notifications_off)
self.history_header = CenterBox(
name="notification-history-header",
spacing=8,
start_children=[self.header_switch, self.dnd_label],
center_children=[self.header_label],
end_children=[self.header_clean],
)
# Create notifications list
self.notifications_list = Box(
name="notifications-list",
orientation="v",
spacing=4,
h_expand=True,
v_expand=True,
h_align="fill",
v_align="fill",
)
# Create "no notifications" label
self.no_notifications_label = Label(
name="no-notif",
markup=icons.notifications_clear,
v_align="fill",
h_align="fill",
v_expand=True,
h_expand=True,
justification="center",
)
self.no_notifications_box = Box(
name="no-notifications-box",
v_align="fill",
h_align="fill",
v_expand=True,
h_expand=True,
children=[self.no_notifications_label],
)
# Add components to main container
self.add(self.history_header)
self.add(self.notifications_list)
# Add the no notifications box initially to the notifications list
self.notifications_list.add(self.no_notifications_box)
# Connect to shared notification history
self._connect_to_notifications()
# Load and sync DND state
self._load_and_sync_dnd_state()
# Load initial notifications
self._load_recent_notifications()
def _connect_to_notifications(self):
"""Connect to the shared notification history for real-time updates"""
try:
self.shared_history = get_shared_notification_history()
self.shared_history.connect("notification-added", self._on_notification_added)
self.shared_history.connect("dnd-state-changed", self._on_dnd_state_changed)
except Exception as e:
print(f"Could not connect to notification history: {e}")
def _on_notification_added(self, shared_history):
"""Handle new notification added"""
GLib.idle_add(self._refresh_notifications)
def _on_dnd_state_changed(self, notification_history, dnd_enabled):
"""Handle DND state change"""
self.do_not_disturb_enabled = dnd_enabled
self.header_switch.set_active(dnd_enabled)
def _load_and_sync_dnd_state(self):
"""Load and sync DND state from shared history"""
try:
if hasattr(self, 'shared_history'):
dnd_enabled = self.shared_history.do_not_disturb_enabled
self.header_switch.set_active(dnd_enabled)
self.do_not_disturb_enabled = dnd_enabled
except Exception as e:
print(f"Could not sync DND state: {e}")
def on_do_not_disturb_changed(self, switch, pspec):
"""Handle DND switch toggle"""
self.do_not_disturb_enabled = switch.get_active()
try:
if hasattr(self, 'shared_history'):
self.shared_history.set_do_not_disturb_enabled(self.do_not_disturb_enabled)
except Exception as e:
print(f"Could not update shared DND state: {e}")
def clear_history(self, *args):
"""Clear all notifications"""
# Clear notification items
for item in self.notification_items[:]:
self.notifications_list.remove(item)
item.destroy()
self.notification_items.clear()
# Clear persistent history
try:
if hasattr(self, 'shared_history'):
self.shared_history.clear_history()
except Exception as e:
print(f"Could not clear shared history: {e}")
# Update visibility
self._update_no_notifications_visibility()
def _load_recent_notifications(self):
"""Load recent notifications from persistent storage"""
try:
if os.path.exists(PERSISTENT_HISTORY_FILE):
with open(PERSISTENT_HISTORY_FILE, "r") as f:
notifications = json.load(f)
# Get the most recent notifications (up to max_notifications)
recent_notifications = notifications[-self.max_notifications:]
for notif_data in reversed(recent_notifications):
self._add_notification_item(notif_data)
except Exception as e:
print(f"Error loading recent notifications: {e}")
def _refresh_notifications(self):
"""Refresh the notification list"""
# Clear existing notifications
for item in self.notification_items[:]:
self.notifications_list.remove(item)
item.destroy()
self.notification_items.clear()
# Reload recent notifications
self._load_recent_notifications()
# Update visibility
self._update_no_notifications_visibility()
def _add_notification_item(self, notification_data):
"""Add a notification item to the dashboard"""
# Remove no notifications box if it's showing
if self.no_notifications_box in self.notifications_list.get_children():
self.notifications_list.remove(self.no_notifications_box)
if len(self.notification_items) >= self.max_notifications:
# Remove oldest notification
oldest = self.notification_items.pop(0)
self.notifications_list.remove(oldest)
oldest.destroy()
# Create new notification item
item = DashboardNotificationItem(notification_data)
self.notification_items.append(item)
self.notifications_list.add(item)
# Update visibility
self._update_no_notifications_visibility()
# Show all to ensure visibility
self.show_all()
def _update_no_notifications_visibility(self):
"""Update visibility of no notifications label"""
has_notifications = bool(self.notification_items)
if has_notifications:
# Hide no notifications box
if self.no_notifications_box in self.notifications_list.get_children():
self.notifications_list.remove(self.no_notifications_box)
else:
# Show no notifications box
if self.no_notifications_box not in self.notifications_list.get_children():
self.notifications_list.add(self.no_notifications_box)
class Dashboard(Box):
@@ -33,43 +398,23 @@ class Dashboard(Box):
props=self.mini_player, mini_props=self.mini_player.get_mini_view()
)
# self.controls = controls
# self.brightness_revealer_mui = self.controls.get_brightness_slider_mui()
#
self.wavy = self.gtk_wrapper = CenterBox(
orientation="v",
# Create real notifications component
self.dashboard_notifications = DashboardNotifications()
# Create scrolled window for notifications (matching dock notifications style)
self.notifications_scrolled = ScrolledWindow(
name="notification-history-scrolled-window",
child=self.dashboard_notifications,
h_scrollbar_policy="never",
v_scrollbar_policy="automatic",
min_content_size=(400, 200),
max_content_size=(450, 300),
h_expand=True,
v_expand=True,
h_align="fill",
v_align="fill",
center_children=WavyCircle(),
)
self.low_bat_msg = Label(label="Low Battery fam (<15%)")
self.content_box = Box(
orientation="v",
children=[
Label(name="notification-source", label="Modus", h_align=True),
self.low_bat_msg,
],
)
close_btn = Button(
name="close-button",
child=Label(name="close-label", markup=icons.cancel),
tooltip_text="Exit",
on_clicked=lambda *_: print("clicked close"),
)
self.notification_box = Box(
name="notification-box",
style_classes="vertical" if info.VERTICAL else "horizontal",
children=[
Label(name="notificationss-icon", h_align=True, markup=icons.blur),
Box(h_expand=True, children=self.content_box),
Box(children=close_btn),
],
propagate_width=False,
propagate_height=False,
)
self.tiles = Box(
@@ -79,72 +424,23 @@ class Dashboard(Box):
self.mini_player_tile,
],
)
self.low_bat_msg_2 = Label(label="Notification ctl test")
self.content_box_2 = Box(
orientation="v",
children=[
Label(name="notification-source", label="System", h_align=True),
self.low_bat_msg_2,
],
)
close_btn_2 = Button(
name="close-button",
child=Label(name="close-label", markup=icons.cancel),
tooltip_text="Exit",
on_clicked=lambda *_: print("clicked close"),
)
self.notification_box_2 = Box(
name="notification-box",
style_classes="vertical" if info.VERTICAL else "horizontal",
children=[
Label(name="notificationss-icon", h_align=True, markup=icons.blur),
Box(h_expand=True, children=self.content_box_2),
Box(children=close_btn_2),
],
)
self.notification_container = Revealer(
transition_duration=250,
transition_type="slide-down",
style="background-color:blue;",
h_expand=True,
child=Box(
name="notification-history-popup-content",
orientation="v",
spacing=8,
h_expand=True,
v_expand=True,
children=[
Box(
name="notification-container",
orientation="v",
v_expand=True,
style="padding:3px; padding-top:0px",
children=[self.notification_box, self.notification_box_2],
),
Box(
v_expand=True,
h_expand=True,
children=self.wavy,
),
],
children=[self.notifications_scrolled],
style_classes=["notification-history-popup"],
),
child_revealed=True,
)
self.children = [
# Box(
# name="inner",
# orientation="h",
# children=[
# CenterBox(
# style_classes="scale-icon-container",
# center_children=Label(
# style_classes="scale-icon", markup=icons.brightness
# ),
# ),
# self.brightness_revealer_mui,
# ],
# ),
Box(name="inner", children=self.tiles),
Box(
children=[
@@ -174,13 +470,38 @@ class Dashboard(Box):
class DashboardWindow(Window):
visible = Property(bool, flags="read-write", default_value=False)
def __init__(self, **kwargs):
super().__init__(
name="dashboard-window",
layer="top",
anchor="bottom",
exclusivity="none",
keyboard_mode="on-demand",
child=Dashboard(),
visible=True,
all_visible=True,
visible=False,
all_visible=False,
**kwargs,
)
# self.hide()
# Add escape key binding to close dashboard
self.add_keybinding("Escape", lambda *_: self.close_dashboard())
self.hide()
def show_dashboard(self):
self.show_all()
self.visible = True
def close_dashboard(self):
self.hide()
self.visible = False
def toggle_dashboard(self):
if self.visible:
self.close_dashboard()
else:
self.show_dashboard()
+3 -3
View File
@@ -157,9 +157,9 @@
inset 60px 0px 10px rgba(255, 255, 255, 1); */
box-shadow:
inset 0 0 100px rgba(148, 148, 148, 0.2),
inset 70px 0 20px rgba(255, 255, 255, 0.2),
inset 0px 0px 20px 5px rgba(255, 255, 255, 0.2);
inset 0 0 100px rgba(148, 148, 148, 0.3),
inset 70px 0 20px rgba(255, 255, 255, 0.3),
inset 0px 0px 20px 5px rgba(255, 255, 255, 0.3);
border-top-right-radius: 15px;
border-bottom-right-radius: 15px;
border-top-left-radius: 20px;
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env python3
"""
Test script to send sample notifications for testing the dashboard notification display.
"""
import subprocess
import time
import sys
def send_test_notification(app_name, title, body, icon=None):
"""Send a test notification using notify-send"""
cmd = ["notify-send", "-a", app_name, title, body]
if icon:
cmd.extend(["-i", icon])
try:
subprocess.run(cmd, check=True)
print(f"Sent notification: {app_name} - {title}")
except subprocess.CalledProcessError as e:
print(f"Failed to send notification: {e}")
def main():
"""Send a series of test notifications"""
print("Sending test notifications for dashboard...")
# Test notifications with different apps and content
test_notifications = [
("Modus", "Dashboard Test", "This is a test notification for the dashboard"),
("System", "Low Battery", "Battery level is below 15%"),
("Firefox", "Download Complete", "Your file has been downloaded successfully"),
("Spotify", "Now Playing", "♪ Your favorite song is now playing"),
("Telegram", "New Message", "You have received a new message"),
]
for i, (app, title, body) in enumerate(test_notifications):
send_test_notification(app, title, body)
if i < len(test_notifications) - 1: # Don't sleep after the last notification
time.sleep(2) # Wait 2 seconds between notifications
print("\nTest notifications sent!")
print("Open the dashboard (SUPER+G) to see the notifications.")
if __name__ == "__main__":
main()