diff --git a/config/settings/constants.py b/config/settings/constants.py index 1538693..952650d 100644 --- a/config/settings/constants.py +++ b/config/settings/constants.py @@ -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, diff --git a/config/settings/key_bindings_tab.py b/config/settings/key_bindings_tab.py index 3725175..896cd0a 100644 --- a/config/settings/key_bindings_tab.py +++ b/config/settings/key_bindings_tab.py @@ -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", diff --git a/config/settings/utils.py b/config/settings/utils.py index 0c04e1d..7a5a2da 100644 --- a/config/settings/utils.py +++ b/config/settings/utils.py @@ -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 diff --git a/main.css b/main.css index f79b2a3..a257f4c 100644 --- a/main.css +++ b/main.css @@ -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; -} diff --git a/main.py b/main.py index 95bebf4..f7cc6d1 100644 --- a/main.py +++ b/main.py @@ -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 [ diff --git a/modules/dashboard/__init__.py b/modules/dashboard/__init__.py new file mode 100644 index 0000000..0cb5d35 --- /dev/null +++ b/modules/dashboard/__init__.py @@ -0,0 +1,8 @@ +""" +Dashboard module for Modus. +Provides a dashboard interface with tiles and notifications. +""" + +from .main import DashboardWindow + +__all__ = ["DashboardWindow"] diff --git a/modules/dashboard/main.py b/modules/dashboard/main.py index eed5841..b9fe01c 100644 --- a/modules/dashboard/main.py +++ b/modules/dashboard/main.py @@ -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() + + diff --git a/styles/player.css b/styles/player.css index 15c2756..285f7ca 100644 --- a/styles/player.css +++ b/styles/player.css @@ -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; diff --git a/test_dashboard_notifications.py b/test_dashboard_notifications.py new file mode 100644 index 0000000..e11ad13 --- /dev/null +++ b/test_dashboard_notifications.py @@ -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()