Compare commits
39 Commits
app-launch
...
aee5be7f39
| Author | SHA1 | Date | |
|---|---|---|---|
| aee5be7f39 | |||
| d1b6d3a560 | |||
| 0d4c4caf10 | |||
| f1c45a7f8c | |||
| 7962947f80 | |||
| c256931b1d | |||
| 5137379ac9 | |||
| 159eafbc73 | |||
| 7b0a4a56db | |||
| 9666a2f7ae | |||
| 0be71cfddf | |||
| 2d3f97cae1 | |||
| 15077fe6fa | |||
| 34e837562f | |||
| a22f16a84f | |||
| 17d11591ac | |||
| 05f20d65b9 | |||
| 0b3ee96ccf | |||
| dd6feb4170 | |||
| 5cea889af3 | |||
| f781603907 | |||
| 5afada0fb3 | |||
| 055f4ebb96 | |||
| da2a7d94d8 | |||
| c814eb01de | |||
| 03598694fc | |||
| c4e522f17a | |||
| fe87de7580 | |||
| 5c2ee58f4d | |||
| 4fda2670ac | |||
| d9a176d4ec | |||
| 1a24c4eb99 | |||
| 0ce3d286e2 | |||
| 7a6eca395d | |||
| 40ab13ab26 | |||
| 56c35ec7ec | |||
| e3396be9af | |||
| fc264dda44 | |||
| df2bef7685 |
2
Makefile
Normal file
2
Makefile
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
run:
|
||||||
|
python -m bar.main --config ./example-stylix-dev.yaml
|
||||||
11
README.md
11
README.md
@@ -1,3 +1,14 @@
|
|||||||
# Todo
|
# Todo
|
||||||
|
|
||||||
|
# Ideas
|
||||||
|
## Org-mode integration
|
||||||
- https://github.com/jlumpe/pyorg
|
- https://github.com/jlumpe/pyorg
|
||||||
- https://github.com/jlumpe/ox-json
|
- https://github.com/jlumpe/ox-json
|
||||||
|
## Emails not seen
|
||||||
|
with notmuch
|
||||||
|
## notch power bar
|
||||||
|
- show the power around the notch
|
||||||
|
- show watts charging/discharging in bar
|
||||||
|
- https://lo.cafe/notchnook
|
||||||
|
## Screenshot menu
|
||||||
|
## Media Playing
|
||||||
|
|||||||
@@ -50,3 +50,11 @@ if app_config is None:
|
|||||||
raise Exception("Config file missing")
|
raise Exception("Config file missing")
|
||||||
|
|
||||||
VINYL = app_config.get("vinyl", {"enable": False})
|
VINYL = app_config.get("vinyl", {"enable": False})
|
||||||
|
BATTERY = app_config.get("battery", {"enable": False})
|
||||||
|
WINDOW_TITLE = app_config.get("window_title", {"enable": True})
|
||||||
|
STYLIX = app_config.get("stylix", {"enable": False})
|
||||||
|
CALENDAR = app_config.get("calendar", {"enable": True, "khal_path": "khal"})
|
||||||
|
NOTMUCH = app_config.get("notmuch", {"enable": True, "notmuch_path": "notmuch", "emacsclient_command": "emacsclient"})
|
||||||
|
BAR_HEIGHT = app_config.get("height", 40)
|
||||||
|
LOG_LEVEL = app_config.get("logLevel", "WARNING")
|
||||||
|
DEV = app_config.get("dev", False)
|
||||||
|
|||||||
62
bar/main.py
62
bar/main.py
@@ -1,52 +1,88 @@
|
|||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
# Configure logging based on dev flag
|
||||||
|
from .config import DEV, LOG_LEVEL
|
||||||
|
if DEV:
|
||||||
|
# In dev mode, disable fabric logs but keep stylix and bar logs
|
||||||
|
logger.disable("fabric")
|
||||||
|
else:
|
||||||
|
# In production, disable fabric logs but keep bar logs with configurable level
|
||||||
|
import sys
|
||||||
|
logger.disable("fabric")
|
||||||
|
logger.configure(handlers=[{"sink": sys.stderr, "level": LOG_LEVEL, "format": "{time} | {level} | {name}:{function}:{line} - {message}"}])
|
||||||
|
|
||||||
from fabric import Application
|
from fabric import Application
|
||||||
|
from fabric.i3 import I3, I3MessageType
|
||||||
from fabric.system_tray.widgets import SystemTray
|
from fabric.system_tray.widgets import SystemTray
|
||||||
from fabric.widgets.wayland import WaylandWindow as Window
|
from fabric.widgets.wayland import WaylandWindow as Window
|
||||||
from fabric.river.widgets import (
|
|
||||||
get_river_connection,
|
|
||||||
)
|
|
||||||
from fabric.utils import (
|
from fabric.utils import (
|
||||||
get_relative_path,
|
get_relative_path,
|
||||||
)
|
)
|
||||||
from .modules.bar import StatusBar
|
from .modules.bar import StatusBar
|
||||||
from .modules.window_fuzzy import FuzzyWindowFinder
|
from .modules.window_fuzzy import FuzzyWindowFinder
|
||||||
|
from .modules.stylix import get_stylix_css_path
|
||||||
|
from .config import STYLIX
|
||||||
|
from .services.fenster import get_i3_connection
|
||||||
|
|
||||||
|
|
||||||
tray = SystemTray(name="system-tray", spacing=4)
|
tray = SystemTray(name="system-tray", spacing=4)
|
||||||
river = get_river_connection()
|
i3 = get_i3_connection()
|
||||||
|
|
||||||
dummy = Window(visible=False)
|
dummy = Window(visible=False)
|
||||||
finder = FuzzyWindowFinder()
|
finder = FuzzyWindowFinder()
|
||||||
|
|
||||||
bar_windows = []
|
bar_windows = []
|
||||||
|
notmuch_widget = None
|
||||||
|
|
||||||
app = Application("bar", dummy, finder)
|
app = Application("bar", dummy, finder)
|
||||||
|
|
||||||
|
# Load CSS - use Stylix if enabled, otherwise use default
|
||||||
|
if STYLIX.get("enable", False):
|
||||||
|
stylix_css_path = get_stylix_css_path()
|
||||||
|
if stylix_css_path:
|
||||||
|
logger.info("[Bar] Using Stylix CSS")
|
||||||
|
# Load base styles first for structure
|
||||||
|
app.set_stylesheet_from_file(get_relative_path("styles/main.css"))
|
||||||
|
# Then apply Stylix theme colors
|
||||||
|
app.set_stylesheet_from_file(stylix_css_path)
|
||||||
|
else:
|
||||||
|
logger.warning("[Bar] Stylix enabled but CSS generation failed, falling back to default")
|
||||||
|
app.set_stylesheet_from_file(get_relative_path("styles/main.css"))
|
||||||
|
else:
|
||||||
|
logger.info("[Bar] Using default CSS")
|
||||||
app.set_stylesheet_from_file(get_relative_path("styles/main.css"))
|
app.set_stylesheet_from_file(get_relative_path("styles/main.css"))
|
||||||
|
|
||||||
|
|
||||||
def spawn_bars():
|
def spawn_bars():
|
||||||
logger.info("[Bar] Spawning bars after river ready")
|
global notmuch_widget
|
||||||
outputs = river.outputs
|
logger.info("[Bar] Spawning bars")
|
||||||
|
outputs_reply = I3.send_command("", I3MessageType.GET_OUTPUTS)
|
||||||
|
|
||||||
if not outputs:
|
if not (outputs_reply.is_ok and isinstance(outputs_reply.reply, list)):
|
||||||
logger.warning("[Bar] No outputs found — skipping bar spawn")
|
logger.warning("[Bar] Failed to get outputs — skipping bar spawn")
|
||||||
return
|
return
|
||||||
|
|
||||||
output_ids = sorted(outputs.keys())
|
outputs = [o for o in outputs_reply.reply if o.get("active")]
|
||||||
|
|
||||||
for i, output_id in enumerate(output_ids):
|
if not outputs:
|
||||||
bar = StatusBar(display=output_id, tray=tray if i == 0 else None, monitor=i)
|
logger.warning("[Bar] No active outputs found — skipping bar spawn")
|
||||||
|
return
|
||||||
|
|
||||||
|
for i, output in enumerate(outputs):
|
||||||
|
output_name = output.get("name", f"Unknown-{i}")
|
||||||
|
bar = StatusBar(display=output_name, tray=tray if i == 0 else None, monitor=i)
|
||||||
bar_windows.append(bar)
|
bar_windows.append(bar)
|
||||||
|
if i == 0 and bar.notmuch:
|
||||||
|
notmuch_widget = bar.notmuch
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
if river.ready:
|
if i3.ready:
|
||||||
spawn_bars()
|
spawn_bars()
|
||||||
else:
|
else:
|
||||||
river.connect("notify::ready", lambda sender, pspec: spawn_bars())
|
i3.connect("notify::ready", lambda *_: spawn_bars())
|
||||||
|
|
||||||
app.run()
|
app.run()
|
||||||
|
|
||||||
|
|||||||
@@ -1,34 +1,32 @@
|
|||||||
import psutil
|
import psutil
|
||||||
from fabric.widgets.box import Box
|
from fabric.widgets.box import Box
|
||||||
from fabric.widgets.label import Label
|
from fabric.widgets.label import Label
|
||||||
|
from fabric.widgets.image import Image
|
||||||
from fabric.widgets.overlay import Overlay
|
from fabric.widgets.overlay import Overlay
|
||||||
from fabric.widgets.datetime import DateTime
|
from fabric.widgets.datetime import DateTime
|
||||||
from fabric.widgets.centerbox import CenterBox
|
from fabric.widgets.centerbox import CenterBox
|
||||||
from bar.modules.player import Player
|
from bar.modules.player import Player
|
||||||
from bar.modules.vinyl import VinylButton
|
from bar.modules.vinyl import VinylButton
|
||||||
|
from bar.modules.quick_menu import QuickMenuOpener
|
||||||
|
from bar.modules.battery import Battery
|
||||||
|
from bar.modules.calendar import CalendarService, CalendarPopup
|
||||||
|
from bar.modules.notmuch import NotmuchWidget
|
||||||
from fabric.widgets.wayland import WaylandWindow as Window
|
from fabric.widgets.wayland import WaylandWindow as Window
|
||||||
from fabric.system_tray.widgets import SystemTray
|
from fabric.system_tray.widgets import SystemTray
|
||||||
from fabric.river.widgets import (
|
from bar.widgets.fenster import FensterWorkspaces, FensterWorkspaceButton, FensterActiveWindow
|
||||||
RiverWorkspaces,
|
from bar.services.fenster import get_i3_connection
|
||||||
RiverWorkspaceButton,
|
|
||||||
RiverActiveWindow,
|
|
||||||
get_river_connection,
|
|
||||||
)
|
|
||||||
from fabric.utils import (
|
|
||||||
invoke_repeater,
|
|
||||||
)
|
|
||||||
from fabric.widgets.circularprogressbar import CircularProgressBar
|
from fabric.widgets.circularprogressbar import CircularProgressBar
|
||||||
|
from bar.services.system_stats import SystemStatsService
|
||||||
|
|
||||||
from bar.config import VINYL
|
from bar.config import VINYL, BATTERY, BAR_HEIGHT, WINDOW_TITLE, NOTMUCH
|
||||||
|
|
||||||
|
|
||||||
class StatusBar(Window):
|
class StatusBar(Window):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
display: int,
|
display: str,
|
||||||
tray: SystemTray | None = None,
|
tray: SystemTray | None = None,
|
||||||
monitor: int = 1,
|
monitor: int = 1,
|
||||||
river_service=None,
|
|
||||||
):
|
):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
name="bar",
|
name="bar",
|
||||||
@@ -40,22 +38,32 @@ class StatusBar(Window):
|
|||||||
all_visible=False,
|
all_visible=False,
|
||||||
monitor=monitor,
|
monitor=monitor,
|
||||||
)
|
)
|
||||||
if river_service:
|
|
||||||
self.river = river_service
|
|
||||||
else:
|
|
||||||
self.river = get_river_connection()
|
|
||||||
|
|
||||||
self.workspaces = RiverWorkspaces(
|
self.workspaces = FensterWorkspaces(
|
||||||
display,
|
output=display,
|
||||||
name="workspaces",
|
name="workspaces",
|
||||||
spacing=4,
|
spacing=4,
|
||||||
buttons_factory=lambda ws_id: RiverWorkspaceButton(id=ws_id, label=None),
|
|
||||||
river_service=self.river,
|
|
||||||
)
|
)
|
||||||
self.date_time = DateTime(name="date-time", formatters="%d %b - %H:%M")
|
# Create calendar components (refresh every 2 minutes)
|
||||||
|
self.calendar_service = CalendarService(update_interval=120000)
|
||||||
|
self.calendar_popup = CalendarPopup()
|
||||||
|
self.calendar_popup_visible = False
|
||||||
|
|
||||||
|
# Create clickable datetime widget
|
||||||
|
from fabric.widgets.button import Button
|
||||||
|
datetime_widget = DateTime(name="date-time", formatters="%d %b - %H:%M")
|
||||||
|
self.date_time = Button(
|
||||||
|
name="date-time-button",
|
||||||
|
child=datetime_widget,
|
||||||
|
on_clicked=self.toggle_calendar,
|
||||||
|
style="background: transparent; border: none; padding: 0; margin: 0; box-shadow: none;"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Connect calendar service to popup
|
||||||
|
self.calendar_service.connect("events-changed", self.update_calendar_display)
|
||||||
self.system_tray = tray
|
self.system_tray = tray
|
||||||
|
|
||||||
self.active_window = RiverActiveWindow(
|
self.active_window = FensterActiveWindow(
|
||||||
name="active-window",
|
name="active-window",
|
||||||
max_length=50,
|
max_length=50,
|
||||||
style="color: #ffffff; font-size: 14px; font-weight: bold;",
|
style="color: #ffffff; font-size: 14px; font-weight: bold;",
|
||||||
@@ -80,6 +88,20 @@ class StatusBar(Window):
|
|||||||
if VINYL["enable"]:
|
if VINYL["enable"]:
|
||||||
self.vinyl = VinylButton()
|
self.vinyl = VinylButton()
|
||||||
|
|
||||||
|
# Create quick menu button
|
||||||
|
self.quick_menu = QuickMenuOpener(icon_name="open-menu-symbolic")
|
||||||
|
# Setup audio section with vinyl if enabled
|
||||||
|
if self.vinyl:
|
||||||
|
self.quick_menu.get_menu().setup_audio_section(vinyl_service=self.vinyl)
|
||||||
|
|
||||||
|
self.battery = None
|
||||||
|
if BATTERY["enable"]:
|
||||||
|
self.battery = Battery()
|
||||||
|
|
||||||
|
self.notmuch = None
|
||||||
|
if NOTMUCH["enable"]:
|
||||||
|
self.notmuch = NotmuchWidget()
|
||||||
|
|
||||||
self.status_container = Box(
|
self.status_container = Box(
|
||||||
name="widgets-container",
|
name="widgets-container",
|
||||||
spacing=4,
|
spacing=4,
|
||||||
@@ -89,15 +111,24 @@ class StatusBar(Window):
|
|||||||
|
|
||||||
end_container_children = []
|
end_container_children = []
|
||||||
|
|
||||||
if self.vinyl:
|
|
||||||
end_container_children.append(self.vinyl)
|
|
||||||
|
|
||||||
end_container_children.append(self.status_container)
|
end_container_children.append(self.status_container)
|
||||||
if self.system_tray:
|
if self.system_tray:
|
||||||
end_container_children.append(self.system_tray)
|
end_container_children.append(self.system_tray)
|
||||||
|
|
||||||
|
if self.battery:
|
||||||
|
end_container_children.append(self.battery)
|
||||||
|
|
||||||
|
if self.notmuch:
|
||||||
|
end_container_children.append(self.notmuch)
|
||||||
|
|
||||||
|
# Add quick menu button next to time
|
||||||
|
end_container_children.append(self.quick_menu)
|
||||||
end_container_children.append(self.date_time)
|
end_container_children.append(self.date_time)
|
||||||
|
|
||||||
|
center_children = []
|
||||||
|
if WINDOW_TITLE["enable"]:
|
||||||
|
center_children.append(self.active_window)
|
||||||
|
|
||||||
self.children = CenterBox(
|
self.children = CenterBox(
|
||||||
name="bar-inner",
|
name="bar-inner",
|
||||||
start_children=Box(
|
start_children=Box(
|
||||||
@@ -105,7 +136,7 @@ class StatusBar(Window):
|
|||||||
spacing=6,
|
spacing=6,
|
||||||
orientation="h",
|
orientation="h",
|
||||||
children=[
|
children=[
|
||||||
Label(name="nixos-label", markup=""),
|
Image(name="nixos-label", icon_name="nix-snowflake-white", icon_size=20),
|
||||||
self.workspaces,
|
self.workspaces,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -113,7 +144,7 @@ class StatusBar(Window):
|
|||||||
name="center-container",
|
name="center-container",
|
||||||
spacing=4,
|
spacing=4,
|
||||||
orientation="h",
|
orientation="h",
|
||||||
children=[self.active_window],
|
children=center_children,
|
||||||
),
|
),
|
||||||
end_children=Box(
|
end_children=Box(
|
||||||
name="end-container",
|
name="end-container",
|
||||||
@@ -123,11 +154,44 @@ class StatusBar(Window):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
invoke_repeater(1000, self.update_progress_bars)
|
# Create system stats service with signal-based updates
|
||||||
|
self.system_stats_service = SystemStatsService(update_interval=3000)
|
||||||
|
self.system_stats_service.connect("stats-changed", self.update_progress_bars)
|
||||||
|
|
||||||
|
# Set the bar height
|
||||||
|
self.set_size_request(-1, BAR_HEIGHT)
|
||||||
|
|
||||||
self.show_all()
|
self.show_all()
|
||||||
|
|
||||||
def update_progress_bars(self):
|
def __del__(self):
|
||||||
self.ram_progress_bar.value = psutil.virtual_memory().percent / 100
|
"""Cleanup when bar is destroyed"""
|
||||||
self.cpu_progress_bar.value = psutil.cpu_percent() / 100
|
if hasattr(self, 'calendar_service'):
|
||||||
return True
|
self.calendar_service.stop_monitoring()
|
||||||
|
|
||||||
|
def update_progress_bars(self, service, cpu_percent, memory_percent):
|
||||||
|
"""Update progress bars when system stats change"""
|
||||||
|
self.cpu_progress_bar.value = cpu_percent
|
||||||
|
self.ram_progress_bar.value = memory_percent
|
||||||
|
|
||||||
|
def toggle_calendar(self, button=None):
|
||||||
|
"""Toggle the calendar popup when datetime is clicked"""
|
||||||
|
from loguru import logger
|
||||||
|
logger.info(f"[Calendar] DateTime clicked, popup_visible: {self.calendar_popup_visible}")
|
||||||
|
|
||||||
|
if self.calendar_popup_visible:
|
||||||
|
logger.info("[Calendar] Hiding calendar popup")
|
||||||
|
self.calendar_popup.set_visible(False)
|
||||||
|
self.calendar_popup_visible = False
|
||||||
|
else:
|
||||||
|
logger.info("[Calendar] Showing calendar popup")
|
||||||
|
# Use cached events - no need to refresh on click
|
||||||
|
cached_events = self.calendar_service.get_cached_events()
|
||||||
|
logger.info(f"[Calendar] Using {len(cached_events)} cached events")
|
||||||
|
self.calendar_popup.update_events_display(cached_events)
|
||||||
|
self.calendar_popup.set_visible(True)
|
||||||
|
self.calendar_popup.show_all()
|
||||||
|
self.calendar_popup_visible = True
|
||||||
|
|
||||||
|
def update_calendar_display(self, service, events):
|
||||||
|
"""Update the calendar popup with events"""
|
||||||
|
self.calendar_popup.update_events_display(events)
|
||||||
|
|||||||
51
bar/modules/battery.py
Normal file
51
bar/modules/battery.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
from gi.repository import GLib
|
||||||
|
from fabric.widgets.box import Box
|
||||||
|
from fabric.widgets.label import Label
|
||||||
|
from fabric.widgets.image import Image
|
||||||
|
from bar.services.battery import BatteryService
|
||||||
|
|
||||||
|
|
||||||
|
class Battery(Box):
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super().__init__(name="battery-widget", orientation="h", spacing=4, **kwargs)
|
||||||
|
|
||||||
|
self.bat_icon = Image(
|
||||||
|
name="bat-icon", icon_name="battery-full-symbolic", icon_size=16
|
||||||
|
)
|
||||||
|
|
||||||
|
self.bat_label = Label(name="bat-label", label="100%")
|
||||||
|
|
||||||
|
# Create battery service with signal-based updates
|
||||||
|
self.battery_service = BatteryService(update_interval=10000) # Check every 10 seconds
|
||||||
|
self.battery_service.connect("battery-changed", self.update_battery)
|
||||||
|
|
||||||
|
self.children = [self.bat_icon, self.bat_label]
|
||||||
|
self.show_all()
|
||||||
|
|
||||||
|
# Initialize with current battery status
|
||||||
|
initial_percent = self.battery_service.percent
|
||||||
|
initial_charging = self.battery_service.charging
|
||||||
|
GLib.idle_add(self.update_battery, None, initial_percent, initial_charging)
|
||||||
|
|
||||||
|
def _icon_lookup(self, bat, charging):
|
||||||
|
# Round to nearest 10 for level-based icons
|
||||||
|
level = max(10, min(100, round(bat / 10) * 10))
|
||||||
|
|
||||||
|
if charging:
|
||||||
|
return f"battery-level-{level}-charging-symbolic"
|
||||||
|
else:
|
||||||
|
return f"battery-level-{level}-symbolic"
|
||||||
|
|
||||||
|
def update_battery(self, service, percent, charging):
|
||||||
|
"""Update battery display when battery status changes"""
|
||||||
|
icon_name = self._icon_lookup(percent, charging)
|
||||||
|
self.bat_icon.set_property("icon-name", icon_name)
|
||||||
|
|
||||||
|
self.bat_label.set_text(f"{int(percent)}%")
|
||||||
|
|
||||||
|
if percent < 20 and not charging:
|
||||||
|
self.bat_label.add_style_class("battery-low")
|
||||||
|
self.bat_icon.add_style_class("battery-low")
|
||||||
|
else:
|
||||||
|
self.bat_label.remove_style_class("battery-low")
|
||||||
|
self.bat_icon.remove_style_class("battery-low")
|
||||||
405
bar/modules/calendar.py
Normal file
405
bar/modules/calendar.py
Normal file
@@ -0,0 +1,405 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import shutil
|
||||||
|
from datetime import datetime, date
|
||||||
|
|
||||||
|
# Add common binary paths to PATH for user binaries
|
||||||
|
os.environ['PATH'] = '/run/current-system/sw/bin:/home/' + os.environ.get('USER', 'user') + '/.nix-profile/bin:' + os.environ.get('PATH', '')
|
||||||
|
from fabric.widgets.box import Box
|
||||||
|
from fabric.widgets.label import Label
|
||||||
|
from fabric.widgets.button import Button
|
||||||
|
from fabric.widgets.image import Image
|
||||||
|
from fabric.widgets.wayland import WaylandWindow as Window
|
||||||
|
from loguru import logger
|
||||||
|
from bar.config import CALENDAR
|
||||||
|
|
||||||
|
# Try to import khal as a Python library
|
||||||
|
try:
|
||||||
|
from khal.cli.main import main_khal
|
||||||
|
from khal.settings import get_config
|
||||||
|
from khal.khalendar import CalendarCollection
|
||||||
|
KHAL_AVAILABLE = True
|
||||||
|
logger.info("[Calendar] Using khal as Python library")
|
||||||
|
except ImportError:
|
||||||
|
KHAL_AVAILABLE = False
|
||||||
|
logger.info("[Calendar] khal Python library not available, falling back to subprocess")
|
||||||
|
|
||||||
|
|
||||||
|
class CalendarService:
|
||||||
|
def __init__(self, update_interval=300000): # 5 minutes default
|
||||||
|
self.events = []
|
||||||
|
self.callbacks = []
|
||||||
|
self._update_interval = update_interval
|
||||||
|
self._timer_id = None
|
||||||
|
|
||||||
|
# Initial load
|
||||||
|
self.update_events()
|
||||||
|
# Start periodic updates
|
||||||
|
self.start_monitoring()
|
||||||
|
|
||||||
|
def connect(self, signal_name, callback):
|
||||||
|
"""Simple callback system to replace signals"""
|
||||||
|
if signal_name == "events-changed":
|
||||||
|
self.callbacks.append(callback)
|
||||||
|
|
||||||
|
def emit_events_changed(self, events):
|
||||||
|
"""Emit events changed to all callbacks"""
|
||||||
|
for callback in self.callbacks:
|
||||||
|
callback(self, events)
|
||||||
|
|
||||||
|
def start_monitoring(self):
|
||||||
|
"""Start periodic event updates"""
|
||||||
|
if self._timer_id is None:
|
||||||
|
from fabric.utils import invoke_repeater
|
||||||
|
|
||||||
|
self._timer_id = invoke_repeater(
|
||||||
|
self._update_interval, self._periodic_update
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
f"[Calendar] Started periodic updates every {self._update_interval/1000/60:.1f} minutes"
|
||||||
|
)
|
||||||
|
|
||||||
|
def stop_monitoring(self):
|
||||||
|
"""Stop periodic event updates"""
|
||||||
|
if self._timer_id is not None:
|
||||||
|
from gi.repository import GLib
|
||||||
|
|
||||||
|
GLib.source_remove(self._timer_id)
|
||||||
|
self._timer_id = None
|
||||||
|
logger.info("[Calendar] Stopped periodic updates")
|
||||||
|
|
||||||
|
def _periodic_update(self):
|
||||||
|
"""Periodic update callback"""
|
||||||
|
logger.info("[Calendar] Performing periodic events update")
|
||||||
|
self.update_events()
|
||||||
|
return True # Keep the timer running
|
||||||
|
|
||||||
|
def get_cached_events(self):
|
||||||
|
"""Get cached events without triggering update"""
|
||||||
|
return self.events
|
||||||
|
|
||||||
|
def update_events_python_api(self):
|
||||||
|
"""Fetch today's events using khal Python API"""
|
||||||
|
try:
|
||||||
|
# Get khal configuration
|
||||||
|
config = get_config()
|
||||||
|
|
||||||
|
# Create calendar collection
|
||||||
|
collection = CalendarCollection.from_calendars(
|
||||||
|
calendars=config['calendars'],
|
||||||
|
dbpath=config['sqlite']['path'],
|
||||||
|
locale=config['locale'],
|
||||||
|
color=config['default']['print_new'],
|
||||||
|
unicode_symbols=config['default']['unicode_symbols'],
|
||||||
|
default_calendar=config['default']['default_calendar'],
|
||||||
|
readonly=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get today's events
|
||||||
|
today = date.today()
|
||||||
|
events = collection.get_events_on(today)
|
||||||
|
|
||||||
|
# Format events to match our expected structure
|
||||||
|
formatted_events = []
|
||||||
|
for event in events:
|
||||||
|
formatted_event = {
|
||||||
|
'title': str(event.summary),
|
||||||
|
'start': event.start.strftime('%m-%d %H:%M') if hasattr(event.start, 'strftime') else '',
|
||||||
|
'end': event.end.strftime('%m-%d %H:%M') if hasattr(event.end, 'strftime') else '',
|
||||||
|
'location': str(event.location) if event.location else ''
|
||||||
|
}
|
||||||
|
formatted_events.append(formatted_event)
|
||||||
|
|
||||||
|
# Sort by start time
|
||||||
|
formatted_events.sort(key=lambda e: e.get('start', ''))
|
||||||
|
|
||||||
|
self.events = formatted_events
|
||||||
|
logger.info(f"[Calendar] Found {len(self.events)} events using Python API")
|
||||||
|
self.emit_events_changed(self.events)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[Calendar] Error using khal Python API: {e}")
|
||||||
|
# Fall back to subprocess method
|
||||||
|
self.update_events_subprocess()
|
||||||
|
|
||||||
|
def update_events_subprocess(self):
|
||||||
|
"""Fetch today's events using khal subprocess (fallback)"""
|
||||||
|
# Get khal path from config
|
||||||
|
khal_path = CALENDAR.get("khal_path", "khal")
|
||||||
|
|
||||||
|
# Check if khal is available
|
||||||
|
if not shutil.which(khal_path):
|
||||||
|
logger.warning(f"[Calendar] khal not found at '{khal_path}'. Please install khal or configure the correct path.")
|
||||||
|
self.events = []
|
||||||
|
self.emit_events_changed(self.events)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
cmd = [
|
||||||
|
khal_path,
|
||||||
|
"list",
|
||||||
|
"--json",
|
||||||
|
"title",
|
||||||
|
"--json",
|
||||||
|
"start",
|
||||||
|
"--json",
|
||||||
|
"end",
|
||||||
|
"--json",
|
||||||
|
"location",
|
||||||
|
"today",
|
||||||
|
]
|
||||||
|
logger.info(f"[Calendar] Running command: {' '.join(cmd)}")
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
logger.info(f"[Calendar] Command stdout: {result.stdout[:200]}...")
|
||||||
|
logger.info(f"[Calendar] Command stderr: {result.stderr[:200]}...")
|
||||||
|
|
||||||
|
if result.stdout.strip():
|
||||||
|
lines = result.stdout.strip().split("\n")
|
||||||
|
all_events = []
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
if line.strip():
|
||||||
|
try:
|
||||||
|
events = json.loads(line)
|
||||||
|
all_events.extend(events)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.events = all_events
|
||||||
|
logger.info(f"[Calendar] Found {len(self.events)} events using subprocess")
|
||||||
|
self.emit_events_changed(self.events)
|
||||||
|
else:
|
||||||
|
self.events = []
|
||||||
|
self.emit_events_changed(self.events)
|
||||||
|
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
logger.error(f"[Calendar] Failed to fetch events: {e}")
|
||||||
|
self.events = []
|
||||||
|
self.emit_events_changed(self.events)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[Calendar] Error processing events: {e}")
|
||||||
|
self.events = []
|
||||||
|
self.emit_events_changed(self.events)
|
||||||
|
|
||||||
|
def update_events(self):
|
||||||
|
"""Fetch today's events from khal"""
|
||||||
|
# Check if calendar is enabled
|
||||||
|
if not CALENDAR.get("enable", True):
|
||||||
|
logger.info("[Calendar] Calendar is disabled in config")
|
||||||
|
self.events = []
|
||||||
|
self.emit_events_changed(self.events)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Try Python API first, fall back to subprocess
|
||||||
|
if KHAL_AVAILABLE:
|
||||||
|
logger.info("[Calendar] Using khal Python API")
|
||||||
|
self.update_events_python_api()
|
||||||
|
else:
|
||||||
|
logger.info("[Calendar] Using khal subprocess")
|
||||||
|
self.update_events_subprocess()
|
||||||
|
|
||||||
|
|
||||||
|
class CalendarPopup(Window):
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super().__init__(
|
||||||
|
name="calendar-popup",
|
||||||
|
layer="top",
|
||||||
|
anchor="top right",
|
||||||
|
margin="10px 10px 0px 0px", # Just a few pixels under the bar
|
||||||
|
exclusivity="none",
|
||||||
|
visible=False,
|
||||||
|
all_visible=False,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Events container
|
||||||
|
self.events_box = Box(
|
||||||
|
name="events-box",
|
||||||
|
orientation="v",
|
||||||
|
spacing=6,
|
||||||
|
style="min-width: 450px; min-height: 200px;",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add a test label to make sure popup is working
|
||||||
|
test_label = Label("Calendar Events", name="calendar-title")
|
||||||
|
|
||||||
|
container = Box(
|
||||||
|
orientation="v", spacing=4, children=[test_label, self.events_box]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.children = container
|
||||||
|
|
||||||
|
# Set explicit size - much bigger
|
||||||
|
self.set_size_request(500, 400)
|
||||||
|
|
||||||
|
def update_events_display(self, events):
|
||||||
|
"""Update the events display"""
|
||||||
|
logger.info(f"[Calendar] Updating popup with {len(events)} events")
|
||||||
|
|
||||||
|
# Clear existing children first
|
||||||
|
self.events_box.children = []
|
||||||
|
|
||||||
|
if not events:
|
||||||
|
logger.info("[Calendar] No events, showing 'no events' message")
|
||||||
|
no_events_label = Label("No events today", name="no-events")
|
||||||
|
self.events_box.add(no_events_label)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check current time for time indicator placement
|
||||||
|
now = datetime.now()
|
||||||
|
current_time = now.strftime("%H:%M")
|
||||||
|
current_time_added = False
|
||||||
|
|
||||||
|
for i, event in enumerate(events):
|
||||||
|
logger.info(f"[Calendar] Processing event {i+1} for display")
|
||||||
|
title = event.get("title", "No title")
|
||||||
|
start_time = event.get("start", "").split()[1] if event.get("start") else ""
|
||||||
|
end_time = event.get("end", "").split()[1] if event.get("end") else ""
|
||||||
|
location = event.get("location", "")
|
||||||
|
|
||||||
|
# Check if we should add current time indicator before this event
|
||||||
|
if not current_time_added and start_time and start_time > current_time:
|
||||||
|
self.add_current_time_indicator(current_time)
|
||||||
|
current_time_added = True
|
||||||
|
|
||||||
|
# Format time display
|
||||||
|
time_str = ""
|
||||||
|
if start_time and end_time:
|
||||||
|
time_str = f"{start_time} - {end_time}"
|
||||||
|
elif start_time:
|
||||||
|
time_str = start_time
|
||||||
|
|
||||||
|
logger.info(f"[Calendar] Creating widget for: {title} ({time_str})")
|
||||||
|
|
||||||
|
# Create event item with horizontal layout - time on left, content on right
|
||||||
|
event_box = Box(
|
||||||
|
name="event-item",
|
||||||
|
orientation="h", # Horizontal layout
|
||||||
|
spacing=12,
|
||||||
|
style_classes=["event-item"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Left side: Time display (fixed width for alignment)
|
||||||
|
time_display = time_str if time_str else "All day"
|
||||||
|
time_label = Label(
|
||||||
|
time_display,
|
||||||
|
name="event-time",
|
||||||
|
style_classes=["event-time"],
|
||||||
|
style="min-width: 100px;" # Fixed width for consistent alignment
|
||||||
|
)
|
||||||
|
|
||||||
|
# Right side: Content (title and location)
|
||||||
|
content_box = Box(
|
||||||
|
name="event-content",
|
||||||
|
orientation="v",
|
||||||
|
spacing=2
|
||||||
|
)
|
||||||
|
|
||||||
|
# Title (no more status prefix)
|
||||||
|
title_label = Label(
|
||||||
|
title,
|
||||||
|
name="event-title",
|
||||||
|
style_classes=["event-title"],
|
||||||
|
)
|
||||||
|
content_box.add(title_label)
|
||||||
|
|
||||||
|
if location:
|
||||||
|
location_label = Label(
|
||||||
|
f"📍 {location}",
|
||||||
|
name="event-location",
|
||||||
|
style_classes=["event-location"],
|
||||||
|
)
|
||||||
|
content_box.add(location_label)
|
||||||
|
|
||||||
|
# Add time and content to the main event box
|
||||||
|
event_box.add(time_label)
|
||||||
|
event_box.add(content_box)
|
||||||
|
|
||||||
|
self.events_box.add(event_box)
|
||||||
|
logger.info(f"[Calendar] Added event widget to events_box")
|
||||||
|
|
||||||
|
# Add current time indicator at the end if not added yet
|
||||||
|
if not current_time_added:
|
||||||
|
self.add_current_time_indicator(current_time)
|
||||||
|
|
||||||
|
# Force refresh the popup display
|
||||||
|
self.events_box.show_all()
|
||||||
|
logger.info(f"[Calendar] Finished updating popup")
|
||||||
|
|
||||||
|
def add_current_time_indicator(self, current_time):
|
||||||
|
"""Add a current time indicator to the events list"""
|
||||||
|
time_indicator = Box(
|
||||||
|
name="current-time-indicator",
|
||||||
|
orientation="h",
|
||||||
|
spacing=8,
|
||||||
|
style_classes=["current-time-indicator"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Current time label
|
||||||
|
time_label = Label(
|
||||||
|
current_time,
|
||||||
|
name="current-time-label",
|
||||||
|
style_classes=["current-time-label"],
|
||||||
|
style="min-width: 100px; font-weight: bold;"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Line indicator
|
||||||
|
line_label = Label(
|
||||||
|
"━━━ NOW",
|
||||||
|
name="current-time-line",
|
||||||
|
style_classes=["current-time-line"],
|
||||||
|
)
|
||||||
|
|
||||||
|
time_indicator.add(time_label)
|
||||||
|
time_indicator.add(line_label)
|
||||||
|
|
||||||
|
self.events_box.add(time_indicator)
|
||||||
|
logger.info(f"[Calendar] Added current time indicator at {current_time}")
|
||||||
|
|
||||||
|
|
||||||
|
class CalendarWidget(Button):
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super().__init__(
|
||||||
|
name="calendar-widget",
|
||||||
|
child=Image(icon_name="x-office-calendar-symbolic", icon_size=16),
|
||||||
|
on_clicked=self.toggle_events,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.service = CalendarService()
|
||||||
|
self.service.connect("events-changed", self.update_events_display)
|
||||||
|
|
||||||
|
# Create popup window
|
||||||
|
self.popup = CalendarPopup()
|
||||||
|
self.popup_visible = False
|
||||||
|
logger.info("[Calendar] Calendar widget initialized with popup")
|
||||||
|
|
||||||
|
# Initial update
|
||||||
|
self.update_events_display(self.service, self.service.events)
|
||||||
|
|
||||||
|
def toggle_events(self, button=None):
|
||||||
|
"""Toggle the visibility of the events popup"""
|
||||||
|
logger.info(f"[Calendar] Button clicked, popup_visible: {self.popup_visible}")
|
||||||
|
|
||||||
|
if self.popup_visible:
|
||||||
|
logger.info("[Calendar] Hiding popup")
|
||||||
|
self.popup.set_visible(False)
|
||||||
|
self.popup_visible = False
|
||||||
|
else:
|
||||||
|
logger.info("[Calendar] Showing popup")
|
||||||
|
# Refresh events when opening
|
||||||
|
self.service.update_events()
|
||||||
|
self.popup.set_visible(True)
|
||||||
|
self.popup.show_all()
|
||||||
|
self.popup_visible = True
|
||||||
|
|
||||||
|
def update_events_display(self, service, events):
|
||||||
|
"""Update the events display in popup"""
|
||||||
|
self.popup.update_events_display(events)
|
||||||
@@ -1,274 +0,0 @@
|
|||||||
import os
|
|
||||||
import struct
|
|
||||||
import subprocess
|
|
||||||
import re
|
|
||||||
import ctypes
|
|
||||||
import signal
|
|
||||||
|
|
||||||
from gi.repository import GLib, Gtk, Gdk
|
|
||||||
from loguru import logger
|
|
||||||
from math import pi
|
|
||||||
|
|
||||||
from fabric.widgets.overlay import Overlay
|
|
||||||
from fabric.utils.helpers import get_relative_path
|
|
||||||
|
|
||||||
import configparser
|
|
||||||
|
|
||||||
|
|
||||||
def get_bars(file_path):
|
|
||||||
config = configparser.ConfigParser()
|
|
||||||
config.read(file_path)
|
|
||||||
return int(config["general"]["bars"])
|
|
||||||
|
|
||||||
|
|
||||||
CAVA_CONFIG = get_relative_path("../config/cavalcade/cava.ini")
|
|
||||||
|
|
||||||
bars = get_bars(CAVA_CONFIG)
|
|
||||||
|
|
||||||
|
|
||||||
def set_death_signal():
|
|
||||||
"""
|
|
||||||
Set the death signal of the child process to SIGTERM so that if the parent
|
|
||||||
process is killed, the child (cava) is automatically terminated.
|
|
||||||
"""
|
|
||||||
libc = ctypes.CDLL("libc.so.6")
|
|
||||||
PR_SET_PDEATHSIG = 1
|
|
||||||
libc.prctl(PR_SET_PDEATHSIG, signal.SIGTERM)
|
|
||||||
|
|
||||||
|
|
||||||
class Cava:
|
|
||||||
"""
|
|
||||||
CAVA wrapper.
|
|
||||||
Launch cava process with certain settings and read output.
|
|
||||||
"""
|
|
||||||
|
|
||||||
NONE = 0
|
|
||||||
RUNNING = 1
|
|
||||||
RESTARTING = 2
|
|
||||||
CLOSING = 3
|
|
||||||
|
|
||||||
def __init__(self, mainapp):
|
|
||||||
self.bars = bars
|
|
||||||
self.path = "/tmp/cava.fifo"
|
|
||||||
|
|
||||||
self.cava_config_file = CAVA_CONFIG
|
|
||||||
self.data_handler = mainapp.draw.update
|
|
||||||
self.command = ["cava", "-p", self.cava_config_file]
|
|
||||||
self.state = self.NONE
|
|
||||||
|
|
||||||
self.env = dict(os.environ)
|
|
||||||
self.env["LC_ALL"] = "en_US.UTF-8" # not sure if it's necessary
|
|
||||||
|
|
||||||
is_16bit = True
|
|
||||||
self.byte_type, self.byte_size, self.byte_norm = (
|
|
||||||
("H", 2, 65535) if is_16bit else ("B", 1, 255)
|
|
||||||
)
|
|
||||||
|
|
||||||
if not os.path.exists(self.path):
|
|
||||||
os.mkfifo(self.path)
|
|
||||||
|
|
||||||
self.fifo_fd = None
|
|
||||||
self.fifo_dummy_fd = None
|
|
||||||
self.io_watch_id = None
|
|
||||||
|
|
||||||
def _run_process(self):
|
|
||||||
logger.debug("Launching cava process...")
|
|
||||||
try:
|
|
||||||
self.process = subprocess.Popen(
|
|
||||||
self.command,
|
|
||||||
stdout=subprocess.DEVNULL,
|
|
||||||
stderr=subprocess.DEVNULL,
|
|
||||||
env=self.env,
|
|
||||||
preexec_fn=set_death_signal, # Ensure cava gets killed when the parent dies.
|
|
||||||
)
|
|
||||||
logger.debug("cava successfully launched!")
|
|
||||||
self.state = self.RUNNING
|
|
||||||
except Exception:
|
|
||||||
logger.exception("Fail to launch cava")
|
|
||||||
|
|
||||||
def _start_io_reader(self):
|
|
||||||
logger.debug("Activating GLib IO watch for cava stream handler")
|
|
||||||
# Open FIFO in non-blocking mode for reading
|
|
||||||
self.fifo_fd = os.open(self.path, os.O_RDONLY | os.O_NONBLOCK)
|
|
||||||
# Open dummy write end to prevent getting an EOF on our FIFO
|
|
||||||
self.fifo_dummy_fd = os.open(self.path, os.O_WRONLY | os.O_NONBLOCK)
|
|
||||||
self.io_watch_id = GLib.io_add_watch(
|
|
||||||
self.fifo_fd, GLib.IO_IN, self._io_callback
|
|
||||||
)
|
|
||||||
|
|
||||||
def _io_callback(self, source, condition):
|
|
||||||
chunk = self.byte_size * self.bars # number of bytes for given format
|
|
||||||
try:
|
|
||||||
data = os.read(self.fifo_fd, chunk)
|
|
||||||
except OSError as e:
|
|
||||||
# logger.error("Error reading FIFO: {}".format(e))
|
|
||||||
return False
|
|
||||||
|
|
||||||
# When no data is read, do not remove the IO watch immediately.
|
|
||||||
if len(data) < chunk:
|
|
||||||
# Instead of closing the FIFO, we log a warning and continue.
|
|
||||||
# logger.warning("Incomplete data packet received (expected {} bytes, got {}). Waiting for more data...".format(chunk, len(data)))
|
|
||||||
# Returning True keeps the IO watch active. A real EOF will only occur when the writer closes.
|
|
||||||
return True
|
|
||||||
|
|
||||||
fmt = self.byte_type * self.bars # format string for struct.unpack
|
|
||||||
sample = [i / self.byte_norm for i in struct.unpack(fmt, data)]
|
|
||||||
GLib.idle_add(self.data_handler, sample)
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _on_stop(self):
|
|
||||||
logger.debug("Cava stream handler deactivated")
|
|
||||||
if self.state == self.RESTARTING:
|
|
||||||
self.start()
|
|
||||||
elif self.state == self.RUNNING:
|
|
||||||
self.state = self.NONE
|
|
||||||
logger.error("Cava process was unexpectedly terminated.")
|
|
||||||
# self.restart() # May cause infinity loop, need more check
|
|
||||||
|
|
||||||
def start(self):
|
|
||||||
"""Launch cava"""
|
|
||||||
self._start_io_reader()
|
|
||||||
self._run_process()
|
|
||||||
|
|
||||||
def restart(self):
|
|
||||||
"""Restart cava process"""
|
|
||||||
if self.state == self.RUNNING:
|
|
||||||
logger.debug("Restarting cava process (normal mode) ...")
|
|
||||||
self.state = self.RESTARTING
|
|
||||||
if self.process.poll() is None:
|
|
||||||
self.process.kill()
|
|
||||||
elif self.state == self.NONE:
|
|
||||||
logger.warning("Restarting cava process (after crash) ...")
|
|
||||||
self.start()
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
"""Stop cava process"""
|
|
||||||
self.state = self.CLOSING
|
|
||||||
if self.process.poll() is None:
|
|
||||||
self.process.kill()
|
|
||||||
if self.io_watch_id:
|
|
||||||
GLib.source_remove(self.io_watch_id)
|
|
||||||
if self.fifo_fd:
|
|
||||||
os.close(self.fifo_fd)
|
|
||||||
if self.fifo_dummy_fd:
|
|
||||||
os.close(self.fifo_dummy_fd)
|
|
||||||
if os.path.exists(self.path):
|
|
||||||
os.remove(self.path)
|
|
||||||
|
|
||||||
|
|
||||||
class AttributeDict(dict):
|
|
||||||
"""Dictionary with keys as attributes. Does nothing but easy reading"""
|
|
||||||
|
|
||||||
def __getattr__(self, attr):
|
|
||||||
return self.get(attr, 3)
|
|
||||||
|
|
||||||
def __setattr__(self, attr, value):
|
|
||||||
self[attr] = value
|
|
||||||
|
|
||||||
|
|
||||||
class Spectrum:
|
|
||||||
"""Spectrum drawing"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.silence_value = 0
|
|
||||||
self.audio_sample = []
|
|
||||||
self.color = None
|
|
||||||
|
|
||||||
self.area = Gtk.DrawingArea()
|
|
||||||
self.area.connect("draw", self.redraw)
|
|
||||||
self.area.add_events(Gdk.EventMask.BUTTON_PRESS_MASK)
|
|
||||||
|
|
||||||
self.sizes = AttributeDict()
|
|
||||||
self.sizes.area = AttributeDict()
|
|
||||||
self.sizes.bar = AttributeDict()
|
|
||||||
|
|
||||||
self.silence = 10
|
|
||||||
self.max_height = 12
|
|
||||||
|
|
||||||
self.area.connect("configure-event", self.size_update)
|
|
||||||
self.color_update()
|
|
||||||
|
|
||||||
def is_silence(self, value):
|
|
||||||
"""Check if volume level critically low during last iterations"""
|
|
||||||
self.silence_value = 0 if value > 0 else self.silence_value + 1
|
|
||||||
return self.silence_value > self.silence
|
|
||||||
|
|
||||||
def update(self, data):
|
|
||||||
"""Audio data processing"""
|
|
||||||
self.color_update()
|
|
||||||
self.audio_sample = data
|
|
||||||
if not self.is_silence(self.audio_sample[0]):
|
|
||||||
self.area.queue_draw()
|
|
||||||
elif self.silence_value == (self.silence + 1):
|
|
||||||
self.audio_sample = [0] * self.sizes.number
|
|
||||||
self.area.queue_draw()
|
|
||||||
|
|
||||||
def redraw(self, widget, cr):
|
|
||||||
"""Draw spectrum graph"""
|
|
||||||
cr.set_source_rgba(*self.color)
|
|
||||||
dx = 3
|
|
||||||
|
|
||||||
center_y = self.sizes.area.height / 2 # center vertical of the drawing area
|
|
||||||
for i, value in enumerate(self.audio_sample):
|
|
||||||
width = self.sizes.area.width / self.sizes.number - self.sizes.padding
|
|
||||||
radius = width / 2
|
|
||||||
height = max(self.sizes.bar.height * min(value, 1), self.sizes.zero) / 2
|
|
||||||
if height == self.sizes.zero / 2 + 1:
|
|
||||||
height *= 0.5
|
|
||||||
|
|
||||||
height = min(height, self.max_height)
|
|
||||||
|
|
||||||
# Draw rectangle and arcs for rounded ends
|
|
||||||
cr.rectangle(dx, center_y - height, width, height * 2)
|
|
||||||
cr.arc(dx + radius, center_y - height, radius, 0, 2 * pi)
|
|
||||||
cr.arc(dx + radius, center_y + height, radius, 0, 2 * pi)
|
|
||||||
|
|
||||||
cr.close_path()
|
|
||||||
dx += width + self.sizes.padding
|
|
||||||
cr.fill()
|
|
||||||
|
|
||||||
def size_update(self, *args):
|
|
||||||
"""Update drawing geometry"""
|
|
||||||
self.sizes.number = bars
|
|
||||||
self.sizes.padding = 100 / bars
|
|
||||||
self.sizes.zero = 0
|
|
||||||
|
|
||||||
self.sizes.area.width = self.area.get_allocated_width()
|
|
||||||
self.sizes.area.height = self.area.get_allocated_height() - 2
|
|
||||||
|
|
||||||
tw = self.sizes.area.width - self.sizes.padding * (self.sizes.number - 1)
|
|
||||||
self.sizes.bar.width = max(int(tw / self.sizes.number), 1)
|
|
||||||
self.sizes.bar.height = self.sizes.area.height
|
|
||||||
|
|
||||||
def color_update(self):
|
|
||||||
"""Set drawing color according to current settings by reading primary color from CSS"""
|
|
||||||
color = "#a5c8ff" # default value
|
|
||||||
try:
|
|
||||||
with open(get_relative_path("../styles/colors.css"), "r") as f:
|
|
||||||
content = f.read()
|
|
||||||
m = re.search(r"--primary:\s*(#[0-9a-fA-F]{6})", content)
|
|
||||||
if m:
|
|
||||||
color = m.group(1)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Failed to read primary color: {}".format(e))
|
|
||||||
red = int(color[1:3], 16) / 255
|
|
||||||
green = int(color[3:5], 16) / 255
|
|
||||||
blue = int(color[5:7], 16) / 255
|
|
||||||
self.color = Gdk.RGBA(red=red, green=green, blue=blue, alpha=1.0)
|
|
||||||
|
|
||||||
|
|
||||||
class SpectrumRender:
|
|
||||||
def __init__(self, mode=None, **kwargs):
|
|
||||||
super().__init__(**kwargs)
|
|
||||||
self.mode = mode
|
|
||||||
|
|
||||||
self.draw = Spectrum()
|
|
||||||
self.cava = Cava(self)
|
|
||||||
self.cava.start()
|
|
||||||
|
|
||||||
def get_spectrum_box(self):
|
|
||||||
# Get the spectrum box
|
|
||||||
box = Overlay(name="cavalcade", h_align="center", v_align="center")
|
|
||||||
box.set_size_request(180, 40)
|
|
||||||
box.add_overlay(self.draw.area)
|
|
||||||
return box
|
|
||||||
178
bar/modules/notmuch.py
Normal file
178
bar/modules/notmuch.py
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
# Add common binary paths to PATH for user binaries
|
||||||
|
os.environ['PATH'] = '/run/current-system/sw/bin:/home/' + os.environ.get('USER', 'user') + '/.nix-profile/bin:' + os.environ.get('PATH', '')
|
||||||
|
from fabric.widgets.box import Box
|
||||||
|
from fabric.widgets.label import Label
|
||||||
|
from fabric.widgets.button import Button
|
||||||
|
from fabric.widgets.image import Image
|
||||||
|
from loguru import logger
|
||||||
|
from bar.config import NOTMUCH
|
||||||
|
|
||||||
|
|
||||||
|
class NotmuchService:
|
||||||
|
def __init__(self, update_interval=60000): # 1 minute default
|
||||||
|
self.unread_count = 0
|
||||||
|
self.callbacks = []
|
||||||
|
self._update_interval = update_interval
|
||||||
|
self._timer_id = None
|
||||||
|
|
||||||
|
# Initial load
|
||||||
|
self.update_unread_count()
|
||||||
|
# Start periodic updates
|
||||||
|
self.start_monitoring()
|
||||||
|
|
||||||
|
def connect(self, signal_name, callback):
|
||||||
|
"""Simple callback system to replace signals"""
|
||||||
|
if signal_name == "unread-changed":
|
||||||
|
self.callbacks.append(callback)
|
||||||
|
|
||||||
|
def emit_unread_changed(self, count):
|
||||||
|
"""Emit unread changed to all callbacks"""
|
||||||
|
for callback in self.callbacks:
|
||||||
|
callback(self, count)
|
||||||
|
|
||||||
|
def start_monitoring(self):
|
||||||
|
"""Start periodic unread count updates"""
|
||||||
|
if self._timer_id is None:
|
||||||
|
from fabric.utils import invoke_repeater
|
||||||
|
|
||||||
|
self._timer_id = invoke_repeater(
|
||||||
|
self._update_interval, self._periodic_update
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
f"[Notmuch] Started periodic updates every {self._update_interval/1000} seconds"
|
||||||
|
)
|
||||||
|
|
||||||
|
def stop_monitoring(self):
|
||||||
|
"""Stop periodic unread count updates"""
|
||||||
|
if self._timer_id is not None:
|
||||||
|
from gi.repository import GLib
|
||||||
|
|
||||||
|
GLib.source_remove(self._timer_id)
|
||||||
|
self._timer_id = None
|
||||||
|
logger.info("[Notmuch] Stopped periodic updates")
|
||||||
|
|
||||||
|
def _periodic_update(self):
|
||||||
|
"""Periodic update callback"""
|
||||||
|
logger.info("[Notmuch] Performing periodic unread count update")
|
||||||
|
self.update_unread_count()
|
||||||
|
return True # Keep the timer running
|
||||||
|
|
||||||
|
def get_cached_count(self):
|
||||||
|
"""Get cached unread count without triggering update"""
|
||||||
|
return self.unread_count
|
||||||
|
|
||||||
|
def update_unread_count(self):
|
||||||
|
"""Fetch unread email count from notmuch"""
|
||||||
|
# Check if notmuch is enabled
|
||||||
|
if not NOTMUCH.get("enable", True):
|
||||||
|
logger.info("[Notmuch] Notmuch is disabled in config")
|
||||||
|
self.unread_count = 0
|
||||||
|
self.emit_unread_changed(self.unread_count)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get notmuch path from config
|
||||||
|
notmuch_path = NOTMUCH.get("notmuch_path", "notmuch")
|
||||||
|
|
||||||
|
# Check if notmuch is available
|
||||||
|
if not shutil.which(notmuch_path):
|
||||||
|
logger.warning(f"[Notmuch] notmuch not found at '{notmuch_path}'. Please install notmuch or configure the correct path.")
|
||||||
|
self.unread_count = 0
|
||||||
|
self.emit_unread_changed(self.unread_count)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Get unread email count
|
||||||
|
cmd = [notmuch_path, "count", "tag:unread"]
|
||||||
|
logger.info(f"[Notmuch] Running command: {' '.join(cmd)}")
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
logger.info(f"[Notmuch] Command stdout: '{result.stdout.strip()}'")
|
||||||
|
logger.info(f"[Notmuch] Command stderr: '{result.stderr.strip()}'")
|
||||||
|
|
||||||
|
if result.stdout.strip():
|
||||||
|
self.unread_count = int(result.stdout.strip())
|
||||||
|
logger.info(f"[Notmuch] Found {self.unread_count} unread emails")
|
||||||
|
self.emit_unread_changed(self.unread_count)
|
||||||
|
else:
|
||||||
|
self.unread_count = 0
|
||||||
|
self.emit_unread_changed(self.unread_count)
|
||||||
|
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
logger.error(f"[Notmuch] Failed to fetch unread count: {e}")
|
||||||
|
self.unread_count = 0
|
||||||
|
self.emit_unread_changed(self.unread_count)
|
||||||
|
except ValueError as e:
|
||||||
|
logger.error(f"[Notmuch] Error parsing unread count: {e}")
|
||||||
|
self.unread_count = 0
|
||||||
|
self.emit_unread_changed(self.unread_count)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[Notmuch] Error getting unread count: {e}")
|
||||||
|
self.unread_count = 0
|
||||||
|
self.emit_unread_changed(self.unread_count)
|
||||||
|
|
||||||
|
|
||||||
|
class NotmuchWidget(Button):
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
# Create the widget content
|
||||||
|
self.icon = Image(icon_name="mail-unread-symbolic", icon_size=16)
|
||||||
|
self.label = Label("0", name="unread-count")
|
||||||
|
|
||||||
|
# Container for icon and label
|
||||||
|
container = Box(
|
||||||
|
orientation="h",
|
||||||
|
spacing=4,
|
||||||
|
children=[self.icon, self.label]
|
||||||
|
)
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
name="notmuch-widget",
|
||||||
|
child=container,
|
||||||
|
on_clicked=self.open_email_client,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Initialize the service
|
||||||
|
self.service = NotmuchService()
|
||||||
|
self.service.connect("unread-changed", self.update_display)
|
||||||
|
|
||||||
|
logger.info("[Notmuch] Notmuch widget initialized")
|
||||||
|
|
||||||
|
# Initial update
|
||||||
|
self.update_display(self.service, self.service.unread_count)
|
||||||
|
|
||||||
|
def open_email_client(self, button=None):
|
||||||
|
"""Open notmuch in emacsclient"""
|
||||||
|
emacsclient_command = NOTMUCH.get("emacsclient_command", "emacsclient")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Open emacsclient with notmuch function
|
||||||
|
cmd = [emacsclient_command, "-c", "-e", "(notmuch)"]
|
||||||
|
logger.info(f"[Notmuch] Running emacsclient command: {' '.join(cmd)}")
|
||||||
|
subprocess.Popen(cmd, start_new_session=True)
|
||||||
|
logger.info(f"[Notmuch] Successfully started emacsclient process")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[Notmuch] Failed to open notmuch in emacsclient '{emacsclient_command}': {e}")
|
||||||
|
|
||||||
|
def update_display(self, service, count):
|
||||||
|
"""Update the widget display with unread count"""
|
||||||
|
# Only show count if there are unread emails
|
||||||
|
if count > 0:
|
||||||
|
self.label.set_text(str(count))
|
||||||
|
self.label.set_visible(True)
|
||||||
|
self.icon.set_from_icon_name("mail-unread-symbolic", 16)
|
||||||
|
self.set_style_classes(["notmuch-widget", "has-unread"])
|
||||||
|
else:
|
||||||
|
self.label.set_text("")
|
||||||
|
self.label.set_visible(False)
|
||||||
|
self.icon.set_from_icon_name("mail-read-symbolic", 16)
|
||||||
|
self.set_style_classes(["notmuch-widget", "no-unread"])
|
||||||
|
|
||||||
|
logger.info(f"[Notmuch] Updated display: {count} unread emails")
|
||||||
@@ -13,10 +13,29 @@ from fabric.widgets.stack import Stack
|
|||||||
from ..widgets.circle_image import CircleImage
|
from ..widgets.circle_image import CircleImage
|
||||||
import bar.modules.icons as icons
|
import bar.modules.icons as icons
|
||||||
from bar.services.mpris import MprisPlayerManager, MprisPlayer
|
from bar.services.mpris import MprisPlayerManager, MprisPlayer
|
||||||
|
from fabric import Fabricator
|
||||||
|
|
||||||
# from bar.modules.cavalcade import SpectrumRender
|
# from bar.modules.cavalcade import SpectrumRender
|
||||||
|
|
||||||
|
|
||||||
|
def get_player_progress(fabricator, mpris_player):
|
||||||
|
"""Get player progress for Fabricator"""
|
||||||
|
if not mpris_player:
|
||||||
|
return (0, 0, 0.0)
|
||||||
|
|
||||||
|
try:
|
||||||
|
current = mpris_player.position
|
||||||
|
except Exception:
|
||||||
|
current = 0
|
||||||
|
try:
|
||||||
|
total = int(mpris_player.length or 0)
|
||||||
|
except Exception:
|
||||||
|
total = 0
|
||||||
|
|
||||||
|
progress = current / total if total > 0 else 0.0
|
||||||
|
return (current, total, progress)
|
||||||
|
|
||||||
|
|
||||||
def get_player_icon_markup_by_name(player_name):
|
def get_player_icon_markup_by_name(player_name):
|
||||||
if player_name:
|
if player_name:
|
||||||
pn = player_name.lower()
|
pn = player_name.lower()
|
||||||
|
|||||||
283
bar/modules/quick_menu.py
Normal file
283
bar/modules/quick_menu.py
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
from fabric.widgets.button import Button
|
||||||
|
from fabric.widgets.image import Image
|
||||||
|
from fabric.widgets.box import Box
|
||||||
|
from fabric.widgets.label import Label
|
||||||
|
from fabric.widgets.wayland import WaylandWindow as Window
|
||||||
|
from gi.repository import Gtk
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
|
||||||
|
class QuickMenuItem(Box):
|
||||||
|
"""Base class for quick menu items"""
|
||||||
|
def __init__(self, title, icon_name=None, **kwargs):
|
||||||
|
super().__init__(
|
||||||
|
orientation="h",
|
||||||
|
spacing=12,
|
||||||
|
name="quick-menu-item",
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
self.set_style("padding: 8px 12px; min-width: 280px;")
|
||||||
|
|
||||||
|
# Icon and title on the left
|
||||||
|
left_box = Box(orientation="h", spacing=8)
|
||||||
|
if icon_name:
|
||||||
|
icon = Image(icon_name=icon_name, icon_size=16)
|
||||||
|
left_box.add(icon)
|
||||||
|
|
||||||
|
self.title_label = Label(title)
|
||||||
|
self.title_label.set_style("font-size: 14px;")
|
||||||
|
left_box.add(self.title_label)
|
||||||
|
|
||||||
|
self.add(left_box)
|
||||||
|
|
||||||
|
# Derived classes can add controls to the right side
|
||||||
|
|
||||||
|
|
||||||
|
class QuickMenuToggle(QuickMenuItem):
|
||||||
|
"""A menu item with a toggle switch"""
|
||||||
|
def __init__(self, title, icon_name=None, active=False, on_toggle=None, **kwargs):
|
||||||
|
super().__init__(title, icon_name, **kwargs)
|
||||||
|
|
||||||
|
# Create a custom toggle using a button with state tracking
|
||||||
|
self._active = active
|
||||||
|
self._on_toggle = on_toggle
|
||||||
|
|
||||||
|
# Create toggle indicator box
|
||||||
|
self.toggle_box = Box(
|
||||||
|
orientation="h",
|
||||||
|
spacing=0
|
||||||
|
)
|
||||||
|
self.toggle_box.set_style("min-width: 44px; min-height: 24px; border-radius: 12px; padding: 2px;")
|
||||||
|
|
||||||
|
# Toggle indicator (circle)
|
||||||
|
self.toggle_indicator = Label("")
|
||||||
|
self.toggle_indicator.set_style("min-width: 20px; min-height: 20px; border-radius: 10px; background: white;")
|
||||||
|
|
||||||
|
self.toggle_box.add(self.toggle_indicator)
|
||||||
|
|
||||||
|
# Make it clickable
|
||||||
|
self.toggle_button = Button(
|
||||||
|
child=self.toggle_box,
|
||||||
|
on_clicked=self._on_click
|
||||||
|
)
|
||||||
|
self.toggle_button.set_style("background: transparent; border: none; padding: 0;")
|
||||||
|
|
||||||
|
# Add spacer to push toggle to the right
|
||||||
|
spacer = Label("", h_expand=True)
|
||||||
|
self.add(spacer)
|
||||||
|
self.add(self.toggle_button)
|
||||||
|
|
||||||
|
# Set initial state
|
||||||
|
self._update_appearance()
|
||||||
|
|
||||||
|
def _on_click(self, button):
|
||||||
|
self._active = not self._active
|
||||||
|
self._update_appearance()
|
||||||
|
if self._on_toggle:
|
||||||
|
self._on_toggle(self._active)
|
||||||
|
|
||||||
|
def _update_appearance(self):
|
||||||
|
if self._active:
|
||||||
|
self.toggle_box.set_style_classes(["toggle-active"])
|
||||||
|
self.toggle_box.set_style(
|
||||||
|
"min-width: 44px; min-height: 24px; border-radius: 12px; padding: 2px; "
|
||||||
|
"transition: all 0.2s;"
|
||||||
|
)
|
||||||
|
self.toggle_indicator.set_style(
|
||||||
|
"min-width: 20px; min-height: 20px; border-radius: 10px; "
|
||||||
|
"background: white; margin-left: 20px; transition: all 0.2s;"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.toggle_box.set_style_classes(["toggle-inactive"])
|
||||||
|
self.toggle_box.set_style(
|
||||||
|
"min-width: 44px; min-height: 24px; border-radius: 12px; padding: 2px; "
|
||||||
|
"transition: all 0.2s;"
|
||||||
|
)
|
||||||
|
self.toggle_indicator.set_style(
|
||||||
|
"min-width: 20px; min-height: 20px; border-radius: 10px; "
|
||||||
|
"background: white; margin-left: 0px; transition: all 0.2s;"
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_active(self, active):
|
||||||
|
self._active = active
|
||||||
|
self._update_appearance()
|
||||||
|
|
||||||
|
def get_active(self):
|
||||||
|
return self._active
|
||||||
|
|
||||||
|
|
||||||
|
class QuickMenuButton(QuickMenuItem):
|
||||||
|
"""A menu item that acts as a button"""
|
||||||
|
def __init__(self, title, icon_name=None, on_click=None, **kwargs):
|
||||||
|
super().__init__(title, icon_name, **kwargs)
|
||||||
|
|
||||||
|
if on_click:
|
||||||
|
# Make the entire item clickable
|
||||||
|
button_overlay = Button(
|
||||||
|
child=Box(), # Empty box as child
|
||||||
|
on_clicked=on_click
|
||||||
|
)
|
||||||
|
button_overlay.set_style("background: transparent; border: none; padding: 0; margin: 0;")
|
||||||
|
|
||||||
|
# Add arrow indicator on the right
|
||||||
|
arrow = Label("›")
|
||||||
|
arrow.set_style("font-size: 18px; opacity: 0.5;")
|
||||||
|
spacer = Label("", h_expand=True)
|
||||||
|
self.add(spacer)
|
||||||
|
self.add(arrow)
|
||||||
|
|
||||||
|
|
||||||
|
class QuickMenuSection(Box):
|
||||||
|
"""A section in the quick menu with optional title"""
|
||||||
|
def __init__(self, title=None, **kwargs):
|
||||||
|
super().__init__(
|
||||||
|
orientation="v",
|
||||||
|
spacing=4,
|
||||||
|
name="quick-menu-section",
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
if title:
|
||||||
|
title_label = Label(
|
||||||
|
title,
|
||||||
|
name="section-title"
|
||||||
|
)
|
||||||
|
title_label.set_style("font-size: 12px; opacity: 0.6; padding: 8px 12px 4px 12px; font-weight: bold;")
|
||||||
|
self.add(title_label)
|
||||||
|
|
||||||
|
self.items_box = Box(orientation="v", spacing=2)
|
||||||
|
self.add(self.items_box)
|
||||||
|
|
||||||
|
def add_item(self, item):
|
||||||
|
self.items_box.add(item)
|
||||||
|
|
||||||
|
|
||||||
|
class QuickMenu(Window):
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super().__init__(
|
||||||
|
name="quick-menu",
|
||||||
|
layer="overlay", # Changed from 'top' to 'overlay' for better shadow support
|
||||||
|
anchor="top right",
|
||||||
|
margin="40px 10px 0px 0px",
|
||||||
|
exclusivity="none",
|
||||||
|
visible=False,
|
||||||
|
all_visible=False,
|
||||||
|
style_classes=["popup-window"],
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Main container
|
||||||
|
self.main_box = Box(
|
||||||
|
orientation="v",
|
||||||
|
spacing=8,
|
||||||
|
name="quick-menu-container"
|
||||||
|
)
|
||||||
|
# Remove redundant styling since it's handled in stylix.css
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Title
|
||||||
|
title_box = Box(
|
||||||
|
orientation="h",
|
||||||
|
spacing=8
|
||||||
|
)
|
||||||
|
title_box.set_style("padding: 12px;")
|
||||||
|
title = Label("Quick Menu")
|
||||||
|
title.set_style("font-size: 16px; font-weight: bold;")
|
||||||
|
title_box.add(title)
|
||||||
|
|
||||||
|
self.main_box.add(title_box)
|
||||||
|
# Add a simple divider line
|
||||||
|
divider = Label("")
|
||||||
|
divider.set_style("min-height: 1px; background: rgba(255,255,255,0.1); margin: 0px 12px;")
|
||||||
|
self.main_box.add(divider)
|
||||||
|
|
||||||
|
# Sections container
|
||||||
|
self.sections_container = Box(
|
||||||
|
orientation="v",
|
||||||
|
spacing=8
|
||||||
|
)
|
||||||
|
self.sections_container.set_style("padding: 8px 0px;")
|
||||||
|
self.main_box.add(self.sections_container)
|
||||||
|
|
||||||
|
self.children = self.main_box
|
||||||
|
self.set_size_request(360, -1)
|
||||||
|
|
||||||
|
# Store references to dynamic items
|
||||||
|
self.vinyl_toggle = None
|
||||||
|
self.sections = {}
|
||||||
|
|
||||||
|
def add_section(self, section_id, title=None):
|
||||||
|
"""Add a new section to the menu"""
|
||||||
|
section = QuickMenuSection(title=title)
|
||||||
|
self.sections[section_id] = section
|
||||||
|
self.sections_container.add(section)
|
||||||
|
|
||||||
|
# Add separator before section if not the first
|
||||||
|
if len(self.sections) > 1:
|
||||||
|
separator = Label("")
|
||||||
|
separator.set_style("min-height: 1px; background: rgba(255,255,255,0.1); margin: 4px 12px;")
|
||||||
|
self.sections_container.add(separator)
|
||||||
|
|
||||||
|
return section
|
||||||
|
|
||||||
|
def setup_audio_section(self, vinyl_service=None):
|
||||||
|
"""Setup the audio controls section"""
|
||||||
|
audio_section = self.add_section("audio", None) # No section title since it's the only section
|
||||||
|
|
||||||
|
# Vinyl passthrough toggle
|
||||||
|
if vinyl_service:
|
||||||
|
self.vinyl_toggle = QuickMenuToggle(
|
||||||
|
title="Vinyl Passthrough",
|
||||||
|
icon_name="folder-music-symbolic",
|
||||||
|
active=vinyl_service.active,
|
||||||
|
on_toggle=lambda active: self._on_vinyl_toggle(active, vinyl_service)
|
||||||
|
)
|
||||||
|
audio_section.add_item(self.vinyl_toggle)
|
||||||
|
|
||||||
|
# Store reference to vinyl service
|
||||||
|
self.vinyl_service = vinyl_service
|
||||||
|
|
||||||
|
def _on_vinyl_toggle(self, active, vinyl_service):
|
||||||
|
"""Handle vinyl toggle"""
|
||||||
|
logger.info(f"[QuickMenu] Vinyl toggled: {active}")
|
||||||
|
vinyl_service.active = active
|
||||||
|
|
||||||
|
def setup_system_section(self):
|
||||||
|
"""Setup system controls section"""
|
||||||
|
# Removed for now - can add system controls later
|
||||||
|
pass
|
||||||
|
|
||||||
|
def update_vinyl_state(self, active):
|
||||||
|
"""Update vinyl toggle state from external source"""
|
||||||
|
if self.vinyl_toggle:
|
||||||
|
self.vinyl_toggle.set_active(active)
|
||||||
|
|
||||||
|
|
||||||
|
class QuickMenuOpener(Button):
|
||||||
|
"""Button to open the quick menu"""
|
||||||
|
def __init__(self, icon_name="open-menu-symbolic", **kwargs):
|
||||||
|
super().__init__(
|
||||||
|
name="quick-menu-button",
|
||||||
|
child=Image(icon_name=icon_name, icon_size=16),
|
||||||
|
on_clicked=self.toggle_menu,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
self.menu = QuickMenu()
|
||||||
|
self.menu_visible = False
|
||||||
|
|
||||||
|
def toggle_menu(self, button=None):
|
||||||
|
"""Toggle the quick menu visibility"""
|
||||||
|
if self.menu_visible:
|
||||||
|
logger.info("[QuickMenu] Hiding menu")
|
||||||
|
self.menu.set_visible(False)
|
||||||
|
self.menu_visible = False
|
||||||
|
else:
|
||||||
|
logger.info("[QuickMenu] Showing menu")
|
||||||
|
self.menu.set_visible(True)
|
||||||
|
self.menu.show_all()
|
||||||
|
self.menu_visible = True
|
||||||
|
|
||||||
|
def get_menu(self):
|
||||||
|
"""Get the menu instance for configuration"""
|
||||||
|
return self.menu
|
||||||
374
bar/modules/stylix.py
Normal file
374
bar/modules/stylix.py
Normal file
@@ -0,0 +1,374 @@
|
|||||||
|
from bar.config import STYLIX
|
||||||
|
import tempfile
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
def generate_stylix_css():
|
||||||
|
"""Generate CSS using Stylix colors if enabled"""
|
||||||
|
if not STYLIX.get("enable", False):
|
||||||
|
return None
|
||||||
|
|
||||||
|
colors = STYLIX.get("colors", {})
|
||||||
|
fonts = STYLIX.get("fonts", {})
|
||||||
|
|
||||||
|
# Default colors if Stylix is not properly configured
|
||||||
|
default_colors = {
|
||||||
|
"base00": "1e1e2e", # background
|
||||||
|
"base01": "313244", # lighter background
|
||||||
|
"base02": "45475a", # selection background
|
||||||
|
"base03": "585b70", # comments
|
||||||
|
"base04": "bac2de", # dark foreground
|
||||||
|
"base05": "cdd6f4", # foreground
|
||||||
|
"base06": "f5e0dc", # light foreground
|
||||||
|
"base07": "b4befe", # light background
|
||||||
|
"base08": "f38ba8", # red
|
||||||
|
"base09": "fab387", # orange
|
||||||
|
"base0A": "f9e2af", # yellow
|
||||||
|
"base0B": "a6e3a1", # green
|
||||||
|
"base0C": "94e2d5", # cyan
|
||||||
|
"base0D": "89b4fa", # blue
|
||||||
|
"base0E": "cba6f7", # purple
|
||||||
|
"base0F": "f2cdcd", # brown
|
||||||
|
}
|
||||||
|
|
||||||
|
# Use Stylix colors or fallback to defaults
|
||||||
|
for key in default_colors:
|
||||||
|
if key not in colors:
|
||||||
|
colors[key] = default_colors[key]
|
||||||
|
|
||||||
|
# Default font
|
||||||
|
font_family = fonts.get("sansSerif", "sans-serif")
|
||||||
|
font_sizes = fonts.get("sizes", {})
|
||||||
|
# Use desktop font size for the bar, fallback to applications, then default
|
||||||
|
font_size = font_sizes.get("desktop", font_sizes.get("applications", 14))
|
||||||
|
|
||||||
|
# Calculate relative font sizes
|
||||||
|
small_font = max(int(font_size * 0.85), 10) # Minimum 10px
|
||||||
|
large_font = int(font_size * 1.1)
|
||||||
|
|
||||||
|
# Debug logging
|
||||||
|
from loguru import logger
|
||||||
|
logger.info(f"[Stylix] Using font sizes - Base: {font_size}px, Small: {small_font}px, Large: {large_font}px")
|
||||||
|
|
||||||
|
# Generate GTK CSS with Stylix colors
|
||||||
|
css_content = f"""/* Stylix-generated theme */
|
||||||
|
|
||||||
|
/* Apply Stylix font */
|
||||||
|
* {{
|
||||||
|
font-family: "{font_family}", sans-serif;
|
||||||
|
font-size: {font_size}px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
/* Bar styling */
|
||||||
|
#bar-inner {{
|
||||||
|
padding: 4px;
|
||||||
|
border-bottom: solid 2px;
|
||||||
|
border-color: #{colors["base02"]};
|
||||||
|
background-color: #{colors["base00"]};
|
||||||
|
}}
|
||||||
|
|
||||||
|
#center-container {{
|
||||||
|
color: #{colors["base05"]};
|
||||||
|
}}
|
||||||
|
|
||||||
|
.active-window {{
|
||||||
|
color: #{colors["base05"]};
|
||||||
|
font-weight: bold;
|
||||||
|
}}
|
||||||
|
|
||||||
|
/* Battery */
|
||||||
|
#battery-widget {{
|
||||||
|
background-color: #{colors["base01"]};
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
#bat-icon {{
|
||||||
|
color: #{colors["base0D"]};
|
||||||
|
margin-right: 2px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
#bat-label {{
|
||||||
|
color: #{colors["base05"]};
|
||||||
|
font-size: {font_size}px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
#bat-label.battery-low {{
|
||||||
|
color: #{colors["base08"]};
|
||||||
|
font-weight: bold;
|
||||||
|
}}
|
||||||
|
|
||||||
|
/* Progress bars */
|
||||||
|
#cpu-progress-bar,
|
||||||
|
#ram-progress-bar,
|
||||||
|
#volume-progress-bar {{
|
||||||
|
color: transparent;
|
||||||
|
background-color: transparent;
|
||||||
|
}}
|
||||||
|
|
||||||
|
#cpu-progress-bar {{
|
||||||
|
border: solid 0px alpha(#{colors["base0E"]}, 0.8);
|
||||||
|
}}
|
||||||
|
|
||||||
|
#ram-progress-bar,
|
||||||
|
#volume-progress-bar {{
|
||||||
|
border: solid 0px #{colors["base0D"]};
|
||||||
|
}}
|
||||||
|
|
||||||
|
/* Widgets container */
|
||||||
|
#widgets-container {{
|
||||||
|
background-color: #{colors["base01"]};
|
||||||
|
padding: 2px;
|
||||||
|
border-radius: 16px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
/* NixOS label */
|
||||||
|
#nixos-label {{
|
||||||
|
color: #{colors["base0D"]};
|
||||||
|
}}
|
||||||
|
|
||||||
|
/* Date time */
|
||||||
|
#date-time {{
|
||||||
|
color: #{colors["base05"]};
|
||||||
|
background-color: #{colors["base01"]};
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
#date-time-button {{
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
}}
|
||||||
|
|
||||||
|
/* Generic popup styling */
|
||||||
|
.popup-window,
|
||||||
|
#calendar-popup,
|
||||||
|
#quick-menu {{
|
||||||
|
background-color: #{colors["base00"]};
|
||||||
|
border: solid 2px #{colors["base02"]};
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4), 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||||
|
animation: slide-down 200ms ease-out;
|
||||||
|
/* Add subtle inner glow for better depth perception */
|
||||||
|
outline: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
outline-offset: -1px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
@keyframes slide-down {{
|
||||||
|
from {{
|
||||||
|
opacity: 0;
|
||||||
|
margin-top: -20px;
|
||||||
|
}}
|
||||||
|
to {{
|
||||||
|
opacity: 1;
|
||||||
|
margin-top: 10px;
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
#calendar-title {{
|
||||||
|
color: #{colors["base05"]};
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: {large_font}px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
#events-box {{
|
||||||
|
background-color: #{colors["base00"]};
|
||||||
|
border: solid 1px #{colors["base02"]};
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
#no-events {{
|
||||||
|
color: #{colors["base03"]};
|
||||||
|
}}
|
||||||
|
|
||||||
|
/* Calendar event items */
|
||||||
|
.event-item {{
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin: 4px 0px;
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
}}
|
||||||
|
|
||||||
|
#event-content {{
|
||||||
|
margin-left: 8px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.event-item.upcoming {{
|
||||||
|
background-color: #{colors["base01"]};
|
||||||
|
}}
|
||||||
|
|
||||||
|
.event-item.past {{
|
||||||
|
background-color: #{colors["base01"]};
|
||||||
|
opacity: 0.6;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.event-title {{
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: {font_size}px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.event-title.upcoming {{
|
||||||
|
color: #{colors["base05"]};
|
||||||
|
}}
|
||||||
|
|
||||||
|
.event-title.past {{
|
||||||
|
color: #{colors["base04"]};
|
||||||
|
}}
|
||||||
|
|
||||||
|
.event-time {{
|
||||||
|
font-size: {small_font}px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.event-time.upcoming {{
|
||||||
|
color: #{colors["base04"]};
|
||||||
|
}}
|
||||||
|
|
||||||
|
.event-time.past {{
|
||||||
|
color: #{colors["base03"]};
|
||||||
|
}}
|
||||||
|
|
||||||
|
.event-location {{
|
||||||
|
font-size: {small_font}px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.event-location.upcoming {{
|
||||||
|
color: #{colors["base03"]};
|
||||||
|
}}
|
||||||
|
|
||||||
|
.event-location.past {{
|
||||||
|
color: #{colors["base03"]};
|
||||||
|
opacity: 0.8;
|
||||||
|
}}
|
||||||
|
|
||||||
|
/* Tooltips */
|
||||||
|
tooltip {{
|
||||||
|
border: solid 2px;
|
||||||
|
border-color: #{colors["base02"]};
|
||||||
|
background-color: #{colors["base00"]};
|
||||||
|
color: #{colors["base05"]};
|
||||||
|
border-radius: 16px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
tooltip>* {{
|
||||||
|
padding: 2px 4px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
/* Workspaces */
|
||||||
|
#workspaces {{
|
||||||
|
background-color: #{colors["base01"]};
|
||||||
|
padding: 6px 6px;
|
||||||
|
border-radius: 16px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
#workspaces>button {{
|
||||||
|
background-color: #{colors["base05"]};
|
||||||
|
border-radius: 100px;
|
||||||
|
padding: 0px 4px;
|
||||||
|
transition: padding 0.05s steps(8);
|
||||||
|
}}
|
||||||
|
|
||||||
|
#workspaces>button.empty:not(.active):not(.visible) {{
|
||||||
|
background-color: #{colors["base03"]};
|
||||||
|
}}
|
||||||
|
|
||||||
|
#workspaces>button.visible:not(.active) {{
|
||||||
|
background-color: #{colors["base0E"]};
|
||||||
|
}}
|
||||||
|
|
||||||
|
#workspaces>button.active {{
|
||||||
|
background-color: #{colors["base0D"]};
|
||||||
|
padding: 0px 16px;
|
||||||
|
border-radius: 100px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
#workspaces>button.urgent {{
|
||||||
|
background-color: #{colors["base08"]};
|
||||||
|
}}
|
||||||
|
|
||||||
|
#workspaces>button>label {{
|
||||||
|
font-size: 0px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
/* Quick Menu styling */
|
||||||
|
#quick-menu-container {{
|
||||||
|
background-color: #{colors["base00"]};
|
||||||
|
border-radius: 8px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
#quick-menu-button {{
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
padding: 4px;
|
||||||
|
margin: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
color: #{colors["base05"]};
|
||||||
|
}}
|
||||||
|
|
||||||
|
#quick-menu-button:hover {{
|
||||||
|
background-color: #{colors["base01"]};
|
||||||
|
border-radius: 8px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.quick-menu-item {{
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.quick-menu-item:hover {{
|
||||||
|
background-color: #{colors["base01"]};
|
||||||
|
}}
|
||||||
|
|
||||||
|
.section-title {{
|
||||||
|
color: #{colors["base04"]};
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: {small_font}px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
/* Vinyl button styling */
|
||||||
|
#vinyl-button {{
|
||||||
|
background-color: transparent;
|
||||||
|
color: #{colors["base05"]};
|
||||||
|
border: none;
|
||||||
|
padding: 4px;
|
||||||
|
margin: 0px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
#vinyl-button.active {{
|
||||||
|
background-color: #{colors["base0B"]};
|
||||||
|
color: #{colors["base00"]};
|
||||||
|
}}
|
||||||
|
|
||||||
|
#vinyl-icon {{
|
||||||
|
color: inherit;
|
||||||
|
}}
|
||||||
|
|
||||||
|
/* Toggle switch styling for quick menu */
|
||||||
|
.toggle-active {{
|
||||||
|
background-color: #{colors["base0B"]};
|
||||||
|
}}
|
||||||
|
|
||||||
|
.toggle-inactive {{
|
||||||
|
background-color: #{colors["base02"]};
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Write to temporary file
|
||||||
|
temp_fd, temp_path = tempfile.mkstemp(suffix=".css", prefix="stylix_")
|
||||||
|
try:
|
||||||
|
with os.fdopen(temp_fd, "w") as f:
|
||||||
|
f.write(css_content)
|
||||||
|
return temp_path
|
||||||
|
except Exception:
|
||||||
|
os.close(temp_fd)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_stylix_css_path():
|
||||||
|
"""Get the path to the Stylix CSS file"""
|
||||||
|
return generate_stylix_css()
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
from fabric.widgets.box import Box
|
from fabric.widgets.button import Button
|
||||||
from fabric.widgets.label import Label
|
from fabric.widgets.image import Image
|
||||||
from fabric.widgets.eventbox import EventBox
|
|
||||||
from fabric.widgets.overlay import Overlay
|
|
||||||
from fabric.core.service import Property
|
from fabric.core.service import Property
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
|
|
||||||
class VinylButton(Box):
|
class VinylButton(Button):
|
||||||
@Property(bool, "read-write", default_value=False)
|
@Property(bool, "read-write", default_value=False)
|
||||||
def active(self) -> bool:
|
def active(self) -> bool:
|
||||||
return self._active
|
return self._active
|
||||||
@@ -25,41 +23,36 @@ class VinylButton(Box):
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
active_command="""pw-link alsa_input.pci-0000_12_00.6.analog-stereo:capture_FL alsa_output.usb-BEHRINGER_UMC1820_A71E9E3E-00.multichannel-output:playback_AUX0
|
active_command=[
|
||||||
pw-link alsa_input.pci-0000_12_00.6.analog-stereo:capture_FR alsa_output.usb-BEHRINGER_UMC1820_A71E9E3E-00.multichannel-output:playback_AUX1""",
|
"pw-link alsa_input.pci-0000_12_00.6.analog-stereo:capture_FL alsa_output.usb-BEHRINGER_UMC1820_A71E9E3E-00.multichannel-output:playback_AUX0",
|
||||||
inactive_command="""pw-link -d alsa_input.pci-0000_12_00.6.analog-stereo:capture_FL alsa_output.usb-BEHRINGER_UMC1820_A71E9E3E-00.multichannel-output:playback_AUX0
|
"pw-link alsa_input.pci-0000_12_00.6.analog-stereo:capture_FR alsa_output.usb-BEHRINGER_UMC1820_A71E9E3E-00.multichannel-output:playback_AUX1",
|
||||||
pw-link -d alsa_input.pci-0000_12_00.6.analog-stereo:capture_FR alsa_output.usb-BEHRINGER_UMC1820_A71E9E3E-00.multichannel-output:playback_AUX1 """,
|
],
|
||||||
|
inactive_command=[
|
||||||
|
"pw-link -d alsa_input.pci-0000_12_00.6.analog-stereo:capture_FL alsa_output.usb-BEHRINGER_UMC1820_A71E9E3E-00.multichannel-output:playback_AUX0",
|
||||||
|
"pw-link -d alsa_input.pci-0000_12_00.6.analog-stereo:capture_FR alsa_output.usb-BEHRINGER_UMC1820_A71E9E3E-00.multichannel-output:playback_AUX1 ",
|
||||||
|
],
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
super().__init__(**kwargs)
|
|
||||||
|
|
||||||
# Initialize properties
|
# Initialize properties
|
||||||
self._active = False
|
self._active = False
|
||||||
self._active_command = active_command
|
self._active_command = active_command
|
||||||
self._inactive_command = inactive_command
|
self._inactive_command = inactive_command
|
||||||
|
|
||||||
# Set up the icon
|
# Set up the icon using GTK icon
|
||||||
self.icon = Label(
|
self.icon = Image(
|
||||||
label="", # CD icon
|
icon_name="folder-music-symbolic",
|
||||||
|
icon_size=16,
|
||||||
name="vinyl-icon",
|
name="vinyl-icon",
|
||||||
style="",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Set up event box to handle clicks
|
# Initialize the Button with the icon as child
|
||||||
self.event_box = EventBox(
|
super().__init__(
|
||||||
events="button-press",
|
|
||||||
child=Overlay(
|
|
||||||
child=self.icon,
|
|
||||||
),
|
|
||||||
name="vinyl-button",
|
name="vinyl-button",
|
||||||
|
child=self.icon,
|
||||||
|
on_clicked=self._on_clicked,
|
||||||
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Connect click event
|
|
||||||
self.event_box.connect("button-press-event", self._on_clicked)
|
|
||||||
|
|
||||||
# Add to parent box
|
|
||||||
self.add(self.event_box)
|
|
||||||
|
|
||||||
# Initialize appearance
|
# Initialize appearance
|
||||||
self._update_appearance()
|
self._update_appearance()
|
||||||
|
|
||||||
@@ -70,23 +63,23 @@ pw-link -d alsa_input.pci-0000_12_00.6.analog-stereo:capture_FR alsa_output.usb-
|
|||||||
else:
|
else:
|
||||||
self.remove_style_class("active")
|
self.remove_style_class("active")
|
||||||
|
|
||||||
def _on_clicked(self, _, event):
|
def _on_clicked(self, button=None):
|
||||||
"""Handle button click event"""
|
"""Handle button click event"""
|
||||||
if event.button == 1: # Left click
|
|
||||||
# Toggle active state
|
# Toggle active state
|
||||||
self.active = not self.active
|
self.active = not self.active
|
||||||
return True
|
|
||||||
|
|
||||||
def _execute_active_command(self):
|
def _execute_active_command(self):
|
||||||
"""Execute shell command when button is activated"""
|
"""Execute shell command when button is activated"""
|
||||||
try:
|
try:
|
||||||
subprocess.Popen(self._active_command, shell=True)
|
for cmd in self._active_command:
|
||||||
|
subprocess.Popen(cmd, shell=True)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error executing active command: {e}")
|
print(f"Error executing active command: {e}")
|
||||||
|
|
||||||
def _execute_inactive_command(self):
|
def _execute_inactive_command(self):
|
||||||
"""Execute shell command when button is deactivated"""
|
"""Execute shell command when button is deactivated"""
|
||||||
try:
|
try:
|
||||||
subprocess.Popen(self._inactive_command, shell=True)
|
for cmd in self._inactive_command:
|
||||||
|
subprocess.Popen(cmd, shell=True)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error executing inactive command: {e}")
|
print(f"Error executing inactive command: {e}")
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import operator
|
from fabric.i3 import I3, I3MessageType
|
||||||
from fabric.widgets.wayland import WaylandWindow as Window
|
from fabric.widgets.wayland import WaylandWindow as Window
|
||||||
from fabric.widgets.box import Box
|
from fabric.widgets.box import Box
|
||||||
from fabric.widgets.label import Label
|
from fabric.widgets.label import Label
|
||||||
from fabric.widgets.entry import Entry
|
from fabric.widgets.entry import Entry
|
||||||
from fabric.utils import idle_add
|
|
||||||
from gi.repository import Gdk
|
from gi.repository import Gdk
|
||||||
|
from bar.services.fenster import get_i3_connection
|
||||||
|
|
||||||
|
|
||||||
class FuzzyWindowFinder(Window):
|
class FuzzyWindowFinder(Window):
|
||||||
@@ -21,7 +21,9 @@ class FuzzyWindowFinder(Window):
|
|||||||
visible=False,
|
visible=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
self._all_windows = ["Test", "Uwu", "Tidal"]
|
self._i3 = get_i3_connection()
|
||||||
|
self._all_windows = []
|
||||||
|
self._refresh_windows()
|
||||||
|
|
||||||
self.viewport = Box(name="viewport", spacing=4, orientation="v")
|
self.viewport = Box(name="viewport", spacing=4, orientation="v")
|
||||||
|
|
||||||
@@ -46,30 +48,81 @@ class FuzzyWindowFinder(Window):
|
|||||||
self.add(self.picker_box)
|
self.add(self.picker_box)
|
||||||
self.arrange_viewport("")
|
self.arrange_viewport("")
|
||||||
|
|
||||||
|
def _refresh_windows(self):
|
||||||
|
"""Refresh the window list via GET_TREE"""
|
||||||
|
self._all_windows = []
|
||||||
|
tree_reply = I3.send_command("", I3MessageType.GET_TREE)
|
||||||
|
if not (tree_reply.is_ok and isinstance(tree_reply.reply, dict)):
|
||||||
|
return
|
||||||
|
|
||||||
|
tree = tree_reply.reply
|
||||||
|
# Traverse: root → outputs → workspaces → containers
|
||||||
|
for output_node in tree.get("nodes", []):
|
||||||
|
for ws_node in output_node.get("nodes", []):
|
||||||
|
ws_num = ws_node.get("num", 0)
|
||||||
|
for con in ws_node.get("nodes", []):
|
||||||
|
if con.get("type") == "con":
|
||||||
|
self._all_windows.append({
|
||||||
|
"id": con.get("id"),
|
||||||
|
"app_id": con.get("app_id", ""),
|
||||||
|
"title": con.get("name", ""),
|
||||||
|
"workspace": ws_num,
|
||||||
|
})
|
||||||
|
for con in ws_node.get("floating_nodes", []):
|
||||||
|
if con.get("type") == "con":
|
||||||
|
self._all_windows.append({
|
||||||
|
"id": con.get("id"),
|
||||||
|
"app_id": con.get("app_id", ""),
|
||||||
|
"title": con.get("name", ""),
|
||||||
|
"workspace": ws_num,
|
||||||
|
})
|
||||||
|
|
||||||
|
def show(self):
|
||||||
|
"""Override show to refresh windows before displaying"""
|
||||||
|
self._refresh_windows()
|
||||||
|
self.arrange_viewport(self.search_entry.get_text())
|
||||||
|
super().show()
|
||||||
|
|
||||||
def notify_text(self, entry, *_):
|
def notify_text(self, entry, *_):
|
||||||
text = entry.get_text()
|
text = entry.get_text()
|
||||||
self.arrange_viewport(text) # Update list on typing
|
self.arrange_viewport(text)
|
||||||
print(text)
|
|
||||||
|
|
||||||
def on_search_entry_key_press(self, widget, event):
|
def on_search_entry_key_press(self, widget, event):
|
||||||
# if event.keyval in (Gdk.KEY_Up, Gdk.KEY_Down, Gdk.KEY_Left, Gdk.KEY_Right):
|
|
||||||
# self.move_selection_2d(event.keyval)
|
|
||||||
# return True
|
|
||||||
print(event.keyval)
|
|
||||||
if event.keyval in [Gdk.KEY_Escape, 103]:
|
if event.keyval in [Gdk.KEY_Escape, 103]:
|
||||||
self.hide()
|
self.hide()
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def on_search_entry_activate(self, text):
|
def on_search_entry_activate(self, text):
|
||||||
print(f"activate {text}")
|
"""Focus the first matching window"""
|
||||||
|
filtered = self._filter_windows(text)
|
||||||
|
if filtered:
|
||||||
|
window_id = filtered[0].get("id")
|
||||||
|
if window_id is not None:
|
||||||
|
I3.send_command(f"[con_id={window_id}] focus")
|
||||||
|
self.hide()
|
||||||
|
|
||||||
|
def _filter_windows(self, query: str) -> list:
|
||||||
|
"""Filter windows based on query matching title or app_id"""
|
||||||
|
if not query:
|
||||||
|
return self._all_windows
|
||||||
|
query_lower = query.lower()
|
||||||
|
return [
|
||||||
|
w for w in self._all_windows
|
||||||
|
if query_lower in w.get("title", "").lower()
|
||||||
|
or query_lower in w.get("app_id", "").lower()
|
||||||
|
]
|
||||||
|
|
||||||
def arrange_viewport(self, query: str = ""):
|
def arrange_viewport(self, query: str = ""):
|
||||||
self.viewport.children = [] # Clear previous entries
|
self.viewport.children = [] # Clear previous entries
|
||||||
|
|
||||||
filtered = [w for w in self._all_windows if query.lower() in w.lower()]
|
filtered = self._filter_windows(query)
|
||||||
|
|
||||||
for window in filtered:
|
for window in filtered:
|
||||||
|
title = window.get("title", "")
|
||||||
|
app_id = window.get("app_id", "")
|
||||||
|
ws_num = window.get("workspace", 0)
|
||||||
|
display_text = f"[{ws_num}] {app_id}: {title}" if app_id else f"[{ws_num}] {title}"
|
||||||
self.viewport.add(
|
self.viewport.add(
|
||||||
Box(name="slot-box", orientation="h", children=[Label(label=window)])
|
Box(name="slot-box", orientation="h", children=[Label(label=display_text)])
|
||||||
)
|
)
|
||||||
|
|||||||
72
bar/services/battery.py
Normal file
72
bar/services/battery.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import psutil
|
||||||
|
from fabric.core.service import Service, Signal
|
||||||
|
from fabric.utils import invoke_repeater
|
||||||
|
|
||||||
|
|
||||||
|
class BatteryService(Service):
|
||||||
|
@Signal
|
||||||
|
def battery_changed(self, percent: float, charging: bool) -> None:
|
||||||
|
"""Signal emitted when battery status changes"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __init__(self, update_interval=10000, **kwargs): # Check every 10 seconds
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self._percent = 0.0
|
||||||
|
self._charging = False
|
||||||
|
self._update_interval = update_interval
|
||||||
|
self._timer_id = None
|
||||||
|
|
||||||
|
# Start periodic updates
|
||||||
|
self.start_monitoring()
|
||||||
|
|
||||||
|
def start_monitoring(self):
|
||||||
|
"""Start monitoring battery status"""
|
||||||
|
if self._timer_id is None:
|
||||||
|
# Get initial values
|
||||||
|
self._update_battery()
|
||||||
|
# Set up periodic updates
|
||||||
|
self._timer_id = invoke_repeater(self._update_interval, self._update_battery)
|
||||||
|
|
||||||
|
def stop_monitoring(self):
|
||||||
|
"""Stop monitoring battery status"""
|
||||||
|
if self._timer_id is not None:
|
||||||
|
from gi.repository import GLib
|
||||||
|
GLib.source_remove(self._timer_id)
|
||||||
|
self._timer_id = None
|
||||||
|
|
||||||
|
def _update_battery(self):
|
||||||
|
"""Update battery status and emit signal if changed"""
|
||||||
|
try:
|
||||||
|
# Use the same pattern as the example
|
||||||
|
bat_sen = psutil.sensors_battery()
|
||||||
|
if not bat_sen:
|
||||||
|
# No battery sensor available (desktop systems)
|
||||||
|
new_percent = 100.0 # Assume plugged in
|
||||||
|
new_charging = True
|
||||||
|
else:
|
||||||
|
new_percent = bat_sen.percent
|
||||||
|
new_charging = bat_sen.power_plugged
|
||||||
|
|
||||||
|
# Only emit signal if values changed
|
||||||
|
percent_changed = abs(new_percent - self._percent) > 0.5
|
||||||
|
charging_changed = new_charging != self._charging
|
||||||
|
|
||||||
|
if percent_changed or charging_changed:
|
||||||
|
self._percent = new_percent
|
||||||
|
self._charging = new_charging
|
||||||
|
self.battery_changed(new_percent, new_charging)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error updating battery status: {e}")
|
||||||
|
|
||||||
|
return True # Keep the timer running
|
||||||
|
|
||||||
|
@property
|
||||||
|
def percent(self):
|
||||||
|
"""Get current battery percentage"""
|
||||||
|
return self._percent
|
||||||
|
|
||||||
|
@property
|
||||||
|
def charging(self):
|
||||||
|
"""Get current charging status"""
|
||||||
|
return self._charging
|
||||||
29
bar/services/fenster.py
Normal file
29
bar/services/fenster.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
"""
|
||||||
|
Fenster/Sway IPC connection helper.
|
||||||
|
|
||||||
|
Provides a singleton I3 connection configured for Fenster's SWAYSOCK.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from fabric.i3 import I3
|
||||||
|
|
||||||
|
|
||||||
|
_connection: I3 | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_i3_connection() -> I3:
|
||||||
|
"""Get the singleton I3 connection, configured for Fenster."""
|
||||||
|
global _connection
|
||||||
|
if _connection is None:
|
||||||
|
swaysock = os.environ.get("SWAYSOCK")
|
||||||
|
if swaysock:
|
||||||
|
I3.SOCKET_PATH = swaysock
|
||||||
|
elif not I3.SOCKET_PATH:
|
||||||
|
runtime_dir = os.environ.get(
|
||||||
|
"XDG_RUNTIME_DIR", f"/run/user/{os.getuid()}"
|
||||||
|
)
|
||||||
|
fallback = os.path.join(runtime_dir, "fenster.sock")
|
||||||
|
if os.path.exists(fallback):
|
||||||
|
I3.SOCKET_PATH = fallback
|
||||||
|
_connection = I3()
|
||||||
|
return _connection
|
||||||
66
bar/services/system_stats.py
Normal file
66
bar/services/system_stats.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import psutil
|
||||||
|
from fabric.core.service import Service, Signal
|
||||||
|
from fabric.utils import invoke_repeater
|
||||||
|
|
||||||
|
|
||||||
|
class SystemStatsService(Service):
|
||||||
|
@Signal
|
||||||
|
def stats_changed(self, cpu_percent: float, memory_percent: float) -> None:
|
||||||
|
"""Signal emitted when system stats change"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __init__(self, update_interval=3000, **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self._cpu_percent = 0.0
|
||||||
|
self._memory_percent = 0.0
|
||||||
|
self._update_interval = update_interval
|
||||||
|
self._timer_id = None
|
||||||
|
|
||||||
|
# Start periodic updates
|
||||||
|
self.start_monitoring()
|
||||||
|
|
||||||
|
def start_monitoring(self):
|
||||||
|
"""Start monitoring system stats"""
|
||||||
|
if self._timer_id is None:
|
||||||
|
# Get initial values
|
||||||
|
self._update_stats()
|
||||||
|
# Set up periodic updates
|
||||||
|
self._timer_id = invoke_repeater(self._update_interval, self._update_stats)
|
||||||
|
|
||||||
|
def stop_monitoring(self):
|
||||||
|
"""Stop monitoring system stats"""
|
||||||
|
if self._timer_id is not None:
|
||||||
|
from gi.repository import GLib
|
||||||
|
|
||||||
|
GLib.source_remove(self._timer_id)
|
||||||
|
self._timer_id = None
|
||||||
|
|
||||||
|
def _update_stats(self):
|
||||||
|
"""Update system stats and emit signal if changed"""
|
||||||
|
try:
|
||||||
|
new_cpu = psutil.cpu_percent()
|
||||||
|
new_memory = psutil.virtual_memory().percent
|
||||||
|
|
||||||
|
# Only emit signal if values changed significantly (reduce noise)
|
||||||
|
cpu_changed = abs(new_cpu - self._cpu_percent) > 1.0
|
||||||
|
memory_changed = abs(new_memory - self._memory_percent) > 1.0
|
||||||
|
|
||||||
|
if cpu_changed or memory_changed:
|
||||||
|
self._cpu_percent = new_cpu
|
||||||
|
self._memory_percent = new_memory
|
||||||
|
self.stats_changed(new_cpu / 100, new_memory / 100)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error updating system stats: {e}")
|
||||||
|
|
||||||
|
return True # Keep the timer running
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cpu_percent(self):
|
||||||
|
"""Get current CPU percentage"""
|
||||||
|
return self._cpu_percent / 100
|
||||||
|
|
||||||
|
@property
|
||||||
|
def memory_percent(self):
|
||||||
|
"""Get current memory percentage"""
|
||||||
|
return self._memory_percent / 100
|
||||||
@@ -3,7 +3,6 @@
|
|||||||
border-bottom: solid 2px;
|
border-bottom: solid 2px;
|
||||||
border-color: var(--border-color);
|
border-color: var(--border-color);
|
||||||
background-color: var(--window-bg);
|
background-color: var(--window-bg);
|
||||||
min-height: 28px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#center-container {
|
#center-container {
|
||||||
@@ -15,6 +14,27 @@
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#battery-widget {
|
||||||
|
background-color: var(--module-bg);
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bat-icon {
|
||||||
|
color: var(--blue);
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bat-label {
|
||||||
|
color: var(--foreground);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bat-label.battery-low {
|
||||||
|
color: var(--red);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
#cpu-progress-bar,
|
#cpu-progress-bar,
|
||||||
#ram-progress-bar,
|
#ram-progress-bar,
|
||||||
#volume-progress-bar {
|
#volume-progress-bar {
|
||||||
|
|||||||
98
bar/styles/calendar.css
Normal file
98
bar/styles/calendar.css
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
/* Calendar widget styling */
|
||||||
|
|
||||||
|
/* Date time button */
|
||||||
|
#date-time-button {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#date-time {
|
||||||
|
color: var(--foreground);
|
||||||
|
background-color: var(--module-bg);
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Calendar popup */
|
||||||
|
#calendar-popup {
|
||||||
|
background-color: var(--window-bg);
|
||||||
|
border: solid 2px var(--border-color);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||||
|
animation: slide-down 200ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slide-down {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
margin-top: -20px;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#calendar-title {
|
||||||
|
color: var(--foreground);
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#events-box {
|
||||||
|
background-color: var(--window-bg);
|
||||||
|
border: none; /* Remove outline */
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#no-events {
|
||||||
|
color: var(--light-grey);
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Calendar event items */
|
||||||
|
.event-item {
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin: 4px 0px;
|
||||||
|
background-color: var(--module-bg);
|
||||||
|
border: none; /* Remove outline */
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#event-content {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-title {
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-time {
|
||||||
|
color: var(--dark-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-location {
|
||||||
|
color: var(--light-grey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Current time indicator */
|
||||||
|
.current-time-indicator {
|
||||||
|
margin: 8px 0px;
|
||||||
|
padding: 4px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#current-time-label {
|
||||||
|
color: var(--blue);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
#current-time-line {
|
||||||
|
color: var(--blue);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
@@ -20,8 +20,9 @@
|
|||||||
--window-bg: alpha(var(--background), 0.9);
|
--window-bg: alpha(var(--background), 0.9);
|
||||||
--module-bg: alpha(var(--mid-bg), 0.8);
|
--module-bg: alpha(var(--mid-bg), 0.8);
|
||||||
--border-color: var(--light-bg);
|
--border-color: var(--light-bg);
|
||||||
--ws-active: var(--pink);
|
--ws-active: var(--blue);
|
||||||
--ws-inactive: var(--blue);
|
--ws-visible: var(--violet);
|
||||||
|
--ws-inactive: var(--light-grey);
|
||||||
--ws-empty: var(--dark-grey);
|
--ws-empty: var(--dark-grey);
|
||||||
--ws-hover: var(--turquoise);
|
--ws-hover: var(--turquoise);
|
||||||
--ws-urgent: var(--red);
|
--ws-urgent: var(--red);
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
@import url("./vinyl.css");
|
@import url("./vinyl.css");
|
||||||
@import url("./bar.css");
|
@import url("./bar.css");
|
||||||
@import url("./finder.css");
|
@import url("./finder.css");
|
||||||
|
@import url("./calendar.css");
|
||||||
|
@import url("./notmuch.css");
|
||||||
|
|
||||||
|
|
||||||
/* unset so we can style everything from the ground up. */
|
/* unset so we can style everything from the ground up. */
|
||||||
|
|||||||
35
bar/styles/notmuch.css
Normal file
35
bar/styles/notmuch.css
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/* Notmuch email widget styling */
|
||||||
|
|
||||||
|
#notmuch-widget {
|
||||||
|
background-color: var(--module-bg);
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#notmuch-widget:hover {
|
||||||
|
background-color: var(--light-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#notmuch-widget.has-unread {
|
||||||
|
background-color: var(--blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
#notmuch-widget.has-unread:hover {
|
||||||
|
background-color: var(--turquoise);
|
||||||
|
}
|
||||||
|
|
||||||
|
#notmuch-widget.no-unread {
|
||||||
|
background-color: var(--module-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#unread-count {
|
||||||
|
color: var(--foreground);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
min-width: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#notmuch-widget.has-unread #unread-count {
|
||||||
|
color: var(--background);
|
||||||
|
}
|
||||||
@@ -1,41 +1,29 @@
|
|||||||
/* Vinyl button styling */
|
/* Vinyl button styling */
|
||||||
#vinyl-button {
|
#vinyl-button {
|
||||||
padding: 0px 8px;
|
background-color: var(--module-bg);
|
||||||
transition: padding 0.05s steps(8);
|
padding: 4px 8px;
|
||||||
background-color: rgba(180, 180, 180, 0.2);
|
border-radius: 12px;
|
||||||
border-radius: 4px;
|
transition: background-color 0.2s ease;
|
||||||
transition: all 0.2s ease;
|
}
|
||||||
|
|
||||||
|
#vinyl-button:hover {
|
||||||
|
background-color: var(--light-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Active state styling */
|
/* Active state styling */
|
||||||
.active #vinyl-button {
|
#vinyl-button.active {
|
||||||
background-color: rgba(108, 158, 175, 0.7);
|
background-color: var(--pink);
|
||||||
padding: 0px 32px;
|
}
|
||||||
|
|
||||||
|
#vinyl-button.active:hover {
|
||||||
|
background-color: var(--turquoise);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Icon styling */
|
/* Icon styling */
|
||||||
#vinyl-icon {
|
#vinyl-icon {
|
||||||
color: #555555;
|
color: var(--foreground);
|
||||||
min-width: 36px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Label styling */
|
#vinyl-button.active #vinyl-icon {
|
||||||
#vinyl-label {
|
color: var(--background);
|
||||||
color: #333333;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Active state changes for icon and label */
|
|
||||||
.active #vinyl-icon,
|
|
||||||
.active #vinyl-label {
|
|
||||||
color: var(--pink);
|
|
||||||
padding: 0px 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hover effect */
|
|
||||||
#vinyl-button:hover {
|
|
||||||
background-color: rgba(180, 180, 180, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.active #vinyl-button:hover {
|
|
||||||
background-color: rgba(108, 158, 175, 0.9);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
|
|
||||||
#workspaces>button {
|
#workspaces>button {
|
||||||
padding: 0px 8px;
|
padding: 0px 8px;
|
||||||
transition: padding 0.05s steps(8);
|
transition: padding 0.05s steps(8), background-color 0.15s ease;
|
||||||
background-color: var(--foreground);
|
background-color: var(--ws-inactive);
|
||||||
border-radius: 100px;
|
border-radius: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15,11 +15,24 @@
|
|||||||
font-size: 0px;
|
font-size: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#workspaces button.hover {
|
#workspaces>button:hover {
|
||||||
background-color: var(--ws-hover);
|
background-color: var(--ws-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
#workspaces button.urgent {
|
#workspaces>button.empty {
|
||||||
|
background-color: var(--ws-empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
#workspaces>button.visible {
|
||||||
|
background-color: var(--ws-visible);
|
||||||
|
}
|
||||||
|
|
||||||
|
#workspaces>button.active {
|
||||||
|
padding: 0px 32px;
|
||||||
|
background-color: var(--ws-active);
|
||||||
|
}
|
||||||
|
|
||||||
|
#workspaces>button.urgent {
|
||||||
background-color: var(--ws-urgent);
|
background-color: var(--ws-urgent);
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@@ -31,12 +44,3 @@
|
|||||||
50% { opacity: 0.5; }
|
50% { opacity: 0.5; }
|
||||||
100% { opacity: 1.0; }
|
100% { opacity: 1.0; }
|
||||||
}
|
}
|
||||||
|
|
||||||
#workspaces>button.empty {
|
|
||||||
background-color: var(--ws-empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
#workspaces>button.active {
|
|
||||||
padding: 0px 32px;
|
|
||||||
background-color: var(--ws-active);
|
|
||||||
}
|
|
||||||
|
|||||||
0
bar/widgets/battery.py
Normal file
0
bar/widgets/battery.py
Normal file
228
bar/widgets/fenster.py
Normal file
228
bar/widgets/fenster.py
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
"""
|
||||||
|
Fenster widgets for workspace and window management via sway IPC.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from gi.repository import GLib
|
||||||
|
|
||||||
|
from fabric.i3 import I3, I3Event, I3MessageType
|
||||||
|
from fabric.utils.helpers import bulk_connect
|
||||||
|
from fabric.widgets.box import Box
|
||||||
|
from fabric.widgets.button import Button
|
||||||
|
from fabric.widgets.label import Label
|
||||||
|
from bar.services.fenster import get_i3_connection
|
||||||
|
|
||||||
|
|
||||||
|
class FensterWorkspaceButton(Button):
|
||||||
|
"""Button representing a single workspace"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
workspace_num: int,
|
||||||
|
i3: I3 | None = None,
|
||||||
|
label: str | None = None,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
self._workspace_num = workspace_num
|
||||||
|
self._i3 = i3 or get_i3_connection()
|
||||||
|
|
||||||
|
display_label = label if label is not None else str(workspace_num)
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
name=f"workspace-button-{workspace_num}",
|
||||||
|
child=Label(label=display_label),
|
||||||
|
on_clicked=self._on_clicked,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.add_style_class("workspace-button")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def workspace_num(self) -> int:
|
||||||
|
return self._workspace_num
|
||||||
|
|
||||||
|
def _on_clicked(self, *args):
|
||||||
|
self._i3.send_command(f"workspace number {self._workspace_num}")
|
||||||
|
|
||||||
|
def _toggle_class(self, name: str, on: bool):
|
||||||
|
if on:
|
||||||
|
self.add_style_class(name)
|
||||||
|
else:
|
||||||
|
self.remove_style_class(name)
|
||||||
|
|
||||||
|
def set_active(self, active: bool):
|
||||||
|
self._toggle_class("active", active)
|
||||||
|
|
||||||
|
def set_visible_other(self, visible: bool):
|
||||||
|
self._toggle_class("visible", visible)
|
||||||
|
|
||||||
|
def set_empty(self, empty: bool):
|
||||||
|
self._toggle_class("empty", empty)
|
||||||
|
|
||||||
|
def set_urgent(self, urgent: bool):
|
||||||
|
self._toggle_class("urgent", urgent)
|
||||||
|
|
||||||
|
|
||||||
|
class FensterWorkspaces(Box):
|
||||||
|
"""Container widget showing a fixed set of workspace bubbles (1..N)."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
output: str | None = None,
|
||||||
|
i3: I3 | None = None,
|
||||||
|
buttons_factory=None,
|
||||||
|
workspace_count: int = 9,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
super().__init__(
|
||||||
|
name=kwargs.pop("name", "workspaces"),
|
||||||
|
spacing=kwargs.pop("spacing", 4),
|
||||||
|
orientation="h",
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._output = output
|
||||||
|
self._workspace_count = workspace_count
|
||||||
|
self._i3 = i3 or get_i3_connection()
|
||||||
|
self._buttons_factory = buttons_factory or self._default_button_factory
|
||||||
|
self._buttons: dict[int, FensterWorkspaceButton] = {}
|
||||||
|
self._refresh_pending = False
|
||||||
|
|
||||||
|
# Pre-create one button per workspace slot so position N always means workspace N.
|
||||||
|
for n in range(1, workspace_count + 1):
|
||||||
|
button = self._buttons_factory(n)
|
||||||
|
self._buttons[n] = button
|
||||||
|
self.add(button)
|
||||||
|
|
||||||
|
bulk_connect(
|
||||||
|
self._i3,
|
||||||
|
{
|
||||||
|
"event::workspace::focus": self._on_event,
|
||||||
|
"event::workspace::init": self._on_event,
|
||||||
|
"event::workspace::empty": self._on_event,
|
||||||
|
"event::workspace::urgent": self._on_event,
|
||||||
|
"event::workspace::move": self._on_event,
|
||||||
|
"event::window::focus": self._on_event,
|
||||||
|
"event::window::new": self._on_event,
|
||||||
|
"event::window::close": self._on_event,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if self._i3.ready:
|
||||||
|
self._schedule_refresh()
|
||||||
|
else:
|
||||||
|
self._i3.connect("notify::ready", lambda *_: self._schedule_refresh())
|
||||||
|
|
||||||
|
def _default_button_factory(self, workspace_num: int) -> FensterWorkspaceButton:
|
||||||
|
return FensterWorkspaceButton(workspace_num=workspace_num, i3=self._i3)
|
||||||
|
|
||||||
|
def _on_event(self, _, event: I3Event):
|
||||||
|
self._schedule_refresh()
|
||||||
|
|
||||||
|
def _schedule_refresh(self):
|
||||||
|
# Defer to the next idle tick — fenster's internal state is not always
|
||||||
|
# updated synchronously when an event fires, so querying GET_WORKSPACES
|
||||||
|
# immediately can return the pre-event view.
|
||||||
|
if self._refresh_pending:
|
||||||
|
return
|
||||||
|
self._refresh_pending = True
|
||||||
|
GLib.idle_add(self._refresh_idle)
|
||||||
|
|
||||||
|
def _refresh_idle(self):
|
||||||
|
self._refresh_pending = False
|
||||||
|
self._refresh_workspaces()
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _refresh_workspaces(self):
|
||||||
|
reply = I3.send_command("", I3MessageType.GET_WORKSPACES)
|
||||||
|
if reply.is_ok and isinstance(reply.reply, list):
|
||||||
|
self._update_workspaces(reply.reply)
|
||||||
|
|
||||||
|
def _update_workspaces(self, workspaces: list):
|
||||||
|
ws_by_num = {
|
||||||
|
ws["num"]: ws for ws in workspaces if ws.get("num") is not None
|
||||||
|
}
|
||||||
|
|
||||||
|
for n, button in self._buttons.items():
|
||||||
|
ws = ws_by_num.get(n)
|
||||||
|
if ws is None:
|
||||||
|
button.set_active(False)
|
||||||
|
button.set_visible_other(False)
|
||||||
|
button.set_urgent(False)
|
||||||
|
button.set_empty(True)
|
||||||
|
continue
|
||||||
|
|
||||||
|
focused = bool(ws.get("focused"))
|
||||||
|
visible = bool(ws.get("visible"))
|
||||||
|
urgent = bool(ws.get("urgent"))
|
||||||
|
window_count = ws.get("window_count", 0)
|
||||||
|
|
||||||
|
button.set_active(focused)
|
||||||
|
# Visible on its output but not the focused one → shown on another monitor.
|
||||||
|
button.set_visible_other(visible and not focused)
|
||||||
|
button.set_urgent(urgent)
|
||||||
|
button.set_empty(window_count == 0)
|
||||||
|
|
||||||
|
self.show_all()
|
||||||
|
|
||||||
|
|
||||||
|
class FensterActiveWindow(Label):
|
||||||
|
"""Label showing the title of the focused window"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
i3: I3 | None = None,
|
||||||
|
max_length: int = 50,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
super().__init__(
|
||||||
|
name=kwargs.pop("name", "active-window"),
|
||||||
|
label="",
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._i3 = i3 or get_i3_connection()
|
||||||
|
self._max_length = max_length
|
||||||
|
|
||||||
|
bulk_connect(
|
||||||
|
self._i3,
|
||||||
|
{
|
||||||
|
"event::window::focus": self._on_window_event,
|
||||||
|
"event::window::title": self._on_window_event,
|
||||||
|
"event::window::close": self._on_window_close,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if self._i3.ready:
|
||||||
|
self._initialize()
|
||||||
|
else:
|
||||||
|
self._i3.connect("notify::ready", lambda *_: self._initialize())
|
||||||
|
|
||||||
|
def _initialize(self):
|
||||||
|
tree_reply = I3.send_command("", I3MessageType.GET_TREE)
|
||||||
|
if tree_reply.is_ok and isinstance(tree_reply.reply, dict):
|
||||||
|
focused = self._find_focused(tree_reply.reply)
|
||||||
|
if focused:
|
||||||
|
self._set_title(focused.get("name", ""))
|
||||||
|
return
|
||||||
|
self.set_label("")
|
||||||
|
|
||||||
|
def _find_focused(self, node: dict) -> dict | None:
|
||||||
|
if node.get("focused") and node.get("type") == "con":
|
||||||
|
return node
|
||||||
|
for child in node.get("nodes", []) + node.get("floating_nodes", []):
|
||||||
|
result = self._find_focused(child)
|
||||||
|
if result:
|
||||||
|
return result
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _on_window_event(self, _, event: I3Event):
|
||||||
|
container = event.data.get("container", {})
|
||||||
|
self._set_title(container.get("name", ""))
|
||||||
|
|
||||||
|
def _on_window_close(self, _, event: I3Event):
|
||||||
|
self._initialize()
|
||||||
|
|
||||||
|
def _set_title(self, title: str):
|
||||||
|
if len(title) > self._max_length:
|
||||||
|
title = title[: self._max_length - 3] + "..."
|
||||||
|
self.set_label(title)
|
||||||
43
example-stylix-dev.yaml
Normal file
43
example-stylix-dev.yaml
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
height: 42
|
||||||
|
dev: true
|
||||||
|
window_title:
|
||||||
|
enable: false
|
||||||
|
vinyl:
|
||||||
|
enable: false
|
||||||
|
battery:
|
||||||
|
enable: true
|
||||||
|
calendar:
|
||||||
|
enable: true
|
||||||
|
khal_path: "khal" # or full path like "/home/user/.nix-profile/bin/khal"
|
||||||
|
notmuch:
|
||||||
|
enable: true
|
||||||
|
notmuch_path: "notmuch" # or full path like "/home/user/.nix-profile/bin/notmuch"
|
||||||
|
emacsclient_command: "emacsclient" # or full path like "/home/user/.nix-profile/bin/emacsclient"
|
||||||
|
stylix:
|
||||||
|
enable: true
|
||||||
|
colors:
|
||||||
|
base00: "1e1e2e" # background
|
||||||
|
base01: "313244" # lighter background
|
||||||
|
base02: "45475a" # selection background
|
||||||
|
base03: "585b70" # comments
|
||||||
|
base04: "bac2de" # dark foreground
|
||||||
|
base05: "cdd6f4" # foreground
|
||||||
|
base06: "f5e0dc" # light foreground
|
||||||
|
base07: "b4befe" # light background
|
||||||
|
base08: "f38ba8" # red
|
||||||
|
base09: "fab387" # orange
|
||||||
|
base0A: "f9e2af" # yellow
|
||||||
|
base0B: "a6e3a1" # green
|
||||||
|
base0C: "94e2d5" # cyan
|
||||||
|
base0D: "89b4fa" # blue
|
||||||
|
base0E: "cba6f7" # purple
|
||||||
|
base0F: "f2cdcd" # brown
|
||||||
|
fonts:
|
||||||
|
sansSerif: "Inter"
|
||||||
|
serif: "Times New Roman"
|
||||||
|
monospace: "JetBrains Mono"
|
||||||
|
sizes:
|
||||||
|
desktop: 16
|
||||||
|
applications: 14
|
||||||
|
terminal: 16
|
||||||
|
popups: 14
|
||||||
@@ -1,2 +1,7 @@
|
|||||||
|
bar_height: 42
|
||||||
|
window_title:
|
||||||
|
enable: false
|
||||||
vinyl:
|
vinyl:
|
||||||
enabled: true
|
enable: true
|
||||||
|
battery:
|
||||||
|
enable: true
|
||||||
|
|||||||
10
flake.lock
generated
10
flake.lock
generated
@@ -6,15 +6,15 @@
|
|||||||
"utils": "utils"
|
"utils": "utils"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1747045720,
|
"lastModified": 1770146720,
|
||||||
"narHash": "sha256-2Z0F4hnluJZunwRfx80EQXpjGLhunV2wrseT42nzh7M=",
|
"narHash": "sha256-YVlwsUz4SLj8qYAb21ernT3lDB/piU1V6hTW/UjikWA=",
|
||||||
"owner": "Makesesama",
|
"owner": "Fabric-Development",
|
||||||
"repo": "fabric",
|
"repo": "fabric",
|
||||||
"rev": "dae50c763e8bf2b4e5807b49b9e62425e0725cfa",
|
"rev": "fd2aabbd7e1859aa7c11c626a6c36a937aca736a",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "Makesesama",
|
"owner": "Fabric-Development",
|
||||||
"repo": "fabric",
|
"repo": "fabric",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
|
|||||||
81
flake.nix
81
flake.nix
@@ -5,7 +5,7 @@
|
|||||||
nixpkgs.url = "github:NixOS/nixpkgs/24.11";
|
nixpkgs.url = "github:NixOS/nixpkgs/24.11";
|
||||||
unstable.url = "github:NixOS/nixpkgs/nixos-unstable";
|
unstable.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
utils.url = "github:numtide/flake-utils";
|
utils.url = "github:numtide/flake-utils";
|
||||||
fabric.url = "github:Makesesama/fabric";
|
fabric.url = "github:Fabric-Development/fabric";
|
||||||
home-manager.url = "github:nix-community/home-manager";
|
home-manager.url = "github:nix-community/home-manager";
|
||||||
home-manager.inputs.nixpkgs.follows = "nixpkgs";
|
home-manager.inputs.nixpkgs.follows = "nixpkgs";
|
||||||
};
|
};
|
||||||
@@ -37,6 +37,9 @@
|
|||||||
makku = pkgs.writeShellScriptBin "makku" ''
|
makku = pkgs.writeShellScriptBin "makku" ''
|
||||||
dbus-send --session --print-reply --dest=org.Fabric.fabric.bar /org/Fabric/fabric org.Fabric.fabric.Evaluate string:"finder.show()" > /dev/null 2>&1
|
dbus-send --session --print-reply --dest=org.Fabric.fabric.bar /org/Fabric/fabric org.Fabric.fabric.Evaluate string:"finder.show()" > /dev/null 2>&1
|
||||||
'';
|
'';
|
||||||
|
notmuch-refresh = pkgs.writeShellScriptBin "notmuch-refresh" ''
|
||||||
|
dbus-send --session --print-reply --dest=org.Fabric.fabric.bar /org/Fabric/fabric org.Fabric.fabric.Evaluate string:"notmuch_widget.service.update_unread_count() if notmuch_widget else None" > /dev/null 2>&1
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
apps.default = {
|
apps.default = {
|
||||||
type = "app";
|
type = "app";
|
||||||
@@ -45,7 +48,8 @@
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
// {
|
// {
|
||||||
homeManagerModules.makku-bar =
|
homeManagerModules = {
|
||||||
|
makku-bar =
|
||||||
{
|
{
|
||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
@@ -76,10 +80,81 @@
|
|||||||
default = false;
|
default = false;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
battery = {
|
||||||
|
enable = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
height = lib.mkOption {
|
||||||
|
type = lib.types.int;
|
||||||
|
default = 40;
|
||||||
|
description = "Height of the status bar in pixels";
|
||||||
|
};
|
||||||
|
logLevel = lib.mkOption {
|
||||||
|
type = lib.types.enum [ "TRACE" "DEBUG" "INFO" "SUCCESS" "WARNING" "ERROR" "CRITICAL" ];
|
||||||
|
default = "WARNING";
|
||||||
|
description = "Log level for the status bar (loguru levels: TRACE, DEBUG, INFO, SUCCESS, WARNING, ERROR, CRITICAL)";
|
||||||
|
};
|
||||||
|
window_title = {
|
||||||
|
enable = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = true;
|
||||||
|
description = "Whether to show the window title in the center of the bar";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
stylix = lib.mkOption {
|
||||||
|
type = lib.types.attrsOf lib.types.anything;
|
||||||
|
default = { enable = false; };
|
||||||
|
description = "Stylix configuration passed from the stylix module";
|
||||||
|
};
|
||||||
|
calendar = {
|
||||||
|
enable = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = true;
|
||||||
|
description = "Whether to enable the calendar widget";
|
||||||
|
};
|
||||||
|
khal_path = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "khal";
|
||||||
|
description = "Path to the khal binary";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
notmuch = {
|
||||||
|
enable = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = true;
|
||||||
|
description = "Whether to enable the notmuch email widget";
|
||||||
|
};
|
||||||
|
notmuch_path = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "notmuch";
|
||||||
|
description = "Path to the notmuch binary";
|
||||||
|
};
|
||||||
|
emacsclient_command = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "emacsclient";
|
||||||
|
description = "Path to the emacsclient binary";
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
default = {
|
default = {
|
||||||
vinyl.enable = false;
|
vinyl.enable = false;
|
||||||
|
battery.enable = false;
|
||||||
|
height = 40;
|
||||||
|
logLevel = "WARNING";
|
||||||
|
window_title.enable = true;
|
||||||
|
stylix.enable = false;
|
||||||
|
calendar = {
|
||||||
|
enable = true;
|
||||||
|
khal_path = "khal";
|
||||||
|
};
|
||||||
|
notmuch = {
|
||||||
|
enable = true;
|
||||||
|
notmuch_path = "notmuch";
|
||||||
|
emacsclient_command = "emacsclient";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -106,5 +181,7 @@
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
stylix-makku-bar = import ./nix/stylix/hm.nix;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,9 @@
|
|||||||
wrapGAppsHook3,
|
wrapGAppsHook3,
|
||||||
playerctl,
|
playerctl,
|
||||||
webp-pixbuf-loader,
|
webp-pixbuf-loader,
|
||||||
|
notmuch,
|
||||||
|
khal,
|
||||||
|
emacs,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
|
|
||||||
@@ -38,6 +41,8 @@ python3Packages.buildPythonApplication {
|
|||||||
gdk-pixbuf
|
gdk-pixbuf
|
||||||
playerctl
|
playerctl
|
||||||
webp-pixbuf-loader
|
webp-pixbuf-loader
|
||||||
|
notmuch
|
||||||
|
khal
|
||||||
];
|
];
|
||||||
|
|
||||||
dependencies = with python3Packages; [
|
dependencies = with python3Packages; [
|
||||||
@@ -60,13 +65,19 @@ python3Packages.buildPythonApplication {
|
|||||||
cp scripts/launcher.py $out/bin/bar
|
cp scripts/launcher.py $out/bin/bar
|
||||||
chmod +x $out/bin/bar
|
chmod +x $out/bin/bar
|
||||||
|
|
||||||
|
|
||||||
runHook postInstall
|
runHook postInstall
|
||||||
'';
|
'';
|
||||||
|
|
||||||
preFixup = ''
|
preFixup = ''
|
||||||
makeWrapperArgs+=("''${gappsWrapperArgs[@]}")
|
makeWrapperArgs+=("''${gappsWrapperArgs[@]}")
|
||||||
|
makeWrapperArgs+=(--prefix PATH : ${lib.makeBinPath [ khal notmuch emacs ]})
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
passthru = {
|
||||||
|
inherit khal notmuch emacs;
|
||||||
|
};
|
||||||
|
|
||||||
meta = {
|
meta = {
|
||||||
changelog = "";
|
changelog = "";
|
||||||
description = ''
|
description = ''
|
||||||
|
|||||||
44
nix/stylix/hm.nix
Normal file
44
nix/stylix/hm.nix
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{ config, lib, ... }:
|
||||||
|
|
||||||
|
let
|
||||||
|
cfg = config.stylix.targets.makku-bar;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.stylix.targets.makku-bar.enable =
|
||||||
|
config.lib.stylix.mkEnableTarget "Makku Bar" true;
|
||||||
|
|
||||||
|
config = lib.mkIf (config.stylix.enable && cfg.enable) {
|
||||||
|
services.makku-bar.settings.stylix = {
|
||||||
|
enable = true;
|
||||||
|
colors = {
|
||||||
|
base00 = config.lib.stylix.colors.base00; # background
|
||||||
|
base01 = config.lib.stylix.colors.base01; # lighter background
|
||||||
|
base02 = config.lib.stylix.colors.base02; # selection background
|
||||||
|
base03 = config.lib.stylix.colors.base03; # comments
|
||||||
|
base04 = config.lib.stylix.colors.base04; # dark foreground
|
||||||
|
base05 = config.lib.stylix.colors.base05; # foreground
|
||||||
|
base06 = config.lib.stylix.colors.base06; # light foreground
|
||||||
|
base07 = config.lib.stylix.colors.base07; # light background
|
||||||
|
base08 = config.lib.stylix.colors.base08; # red
|
||||||
|
base09 = config.lib.stylix.colors.base09; # orange
|
||||||
|
base0A = config.lib.stylix.colors.base0A; # yellow
|
||||||
|
base0B = config.lib.stylix.colors.base0B; # green
|
||||||
|
base0C = config.lib.stylix.colors.base0C; # cyan
|
||||||
|
base0D = config.lib.stylix.colors.base0D; # blue
|
||||||
|
base0E = config.lib.stylix.colors.base0E; # purple
|
||||||
|
base0F = config.lib.stylix.colors.base0F; # brown
|
||||||
|
};
|
||||||
|
fonts = {
|
||||||
|
serif = config.stylix.fonts.serif.name;
|
||||||
|
sansSerif = config.stylix.fonts.sansSerif.name;
|
||||||
|
monospace = config.stylix.fonts.monospace.name;
|
||||||
|
sizes = {
|
||||||
|
desktop = config.stylix.fonts.sizes.desktop;
|
||||||
|
applications = config.stylix.fonts.sizes.applications;
|
||||||
|
terminal = config.stylix.fonts.sizes.terminal;
|
||||||
|
popups = config.stylix.fonts.sizes.popups;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
6
nix/stylix/meta.nix
Normal file
6
nix/stylix/meta.nix
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{ lib }:
|
||||||
|
{
|
||||||
|
name = "Makku Bar";
|
||||||
|
homepage = "https://github.com/Makesesama/makku-bar";
|
||||||
|
maintainers = [ ];
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user