Compare commits
39 Commits
wlr-finder
...
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
|
||||
|
||||
# Ideas
|
||||
## Org-mode integration
|
||||
- https://github.com/jlumpe/pyorg
|
||||
- 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")
|
||||
|
||||
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)
|
||||
|
||||
70
bar/main.py
70
bar/main.py
@@ -1,58 +1,88 @@
|
||||
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.i3 import I3, I3MessageType
|
||||
from fabric.system_tray.widgets import SystemTray
|
||||
from fabric.widgets.wayland import WaylandWindow as Window
|
||||
|
||||
|
||||
from fabric.utils import (
|
||||
get_relative_path,
|
||||
)
|
||||
from .modules.bar import StatusBar
|
||||
from .modules.window_fuzzy import FuzzyWindowFinder
|
||||
from .services.river.widgets import get_river_connection
|
||||
from .services.wlr.event_loop import WaylandEventLoopService
|
||||
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)
|
||||
wayland_event_loop = WaylandEventLoopService()
|
||||
river = get_river_connection()
|
||||
i3 = get_i3_connection()
|
||||
|
||||
dummy = Window(visible=False)
|
||||
finder = FuzzyWindowFinder()
|
||||
|
||||
bar_windows = []
|
||||
notmuch_widget = None
|
||||
|
||||
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"))
|
||||
|
||||
|
||||
def spawn_bars():
|
||||
logger.info("[Bar] Spawning bars after river ready")
|
||||
outputs = river.outputs
|
||||
global notmuch_widget
|
||||
logger.info("[Bar] Spawning bars")
|
||||
outputs_reply = I3.send_command("", I3MessageType.GET_OUTPUTS)
|
||||
|
||||
if not outputs:
|
||||
logger.warning("[Bar] No outputs found — skipping bar spawn")
|
||||
if not (outputs_reply.is_ok and isinstance(outputs_reply.reply, list)):
|
||||
logger.warning("[Bar] Failed to get outputs — skipping bar spawn")
|
||||
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):
|
||||
bar = StatusBar(
|
||||
display=output_id,
|
||||
tray=tray if i == 0 else None,
|
||||
monitor=i,
|
||||
river_service=river,
|
||||
)
|
||||
if not outputs:
|
||||
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)
|
||||
if i == 0 and bar.notmuch:
|
||||
notmuch_widget = bar.notmuch
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
if river.ready:
|
||||
if i3.ready:
|
||||
spawn_bars()
|
||||
else:
|
||||
river.connect("notify::ready", lambda sender, pspec: spawn_bars())
|
||||
i3.connect("notify::ready", lambda *_: spawn_bars())
|
||||
|
||||
app.run()
|
||||
|
||||
|
||||
@@ -1,35 +1,32 @@
|
||||
import psutil
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.label import Label
|
||||
from fabric.widgets.image import Image
|
||||
from fabric.widgets.overlay import Overlay
|
||||
from fabric.widgets.datetime import DateTime
|
||||
from fabric.widgets.centerbox import CenterBox
|
||||
from bar.modules.player import Player
|
||||
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.system_tray.widgets import SystemTray
|
||||
|
||||
from fabric.utils import (
|
||||
invoke_repeater,
|
||||
)
|
||||
from bar.widgets.fenster import FensterWorkspaces, FensterWorkspaceButton, FensterActiveWindow
|
||||
from bar.services.fenster import get_i3_connection
|
||||
from fabric.widgets.circularprogressbar import CircularProgressBar
|
||||
from bar.services.system_stats import SystemStatsService
|
||||
|
||||
from bar.config import VINYL
|
||||
from bar.services.river.widgets import (
|
||||
RiverWorkspaces,
|
||||
RiverWorkspaceButton,
|
||||
RiverActiveWindow,
|
||||
get_river_connection,
|
||||
)
|
||||
from bar.config import VINYL, BATTERY, BAR_HEIGHT, WINDOW_TITLE, NOTMUCH
|
||||
|
||||
|
||||
class StatusBar(Window):
|
||||
def __init__(
|
||||
self,
|
||||
display: int,
|
||||
display: str,
|
||||
tray: SystemTray | None = None,
|
||||
monitor: int = 1,
|
||||
river_service=None,
|
||||
):
|
||||
super().__init__(
|
||||
name="bar",
|
||||
@@ -41,21 +38,32 @@ class StatusBar(Window):
|
||||
all_visible=False,
|
||||
monitor=monitor,
|
||||
)
|
||||
if river_service:
|
||||
self.river = river_service
|
||||
|
||||
self.workspaces = RiverWorkspaces(
|
||||
display,
|
||||
self.workspaces = FensterWorkspaces(
|
||||
output=display,
|
||||
name="workspaces",
|
||||
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.active_window = RiverActiveWindow(
|
||||
river_service=self.river,
|
||||
self.active_window = FensterActiveWindow(
|
||||
name="active-window",
|
||||
max_length=50,
|
||||
style="color: #ffffff; font-size: 14px; font-weight: bold;",
|
||||
@@ -80,6 +88,20 @@ class StatusBar(Window):
|
||||
if VINYL["enable"]:
|
||||
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(
|
||||
name="widgets-container",
|
||||
spacing=4,
|
||||
@@ -89,15 +111,24 @@ class StatusBar(Window):
|
||||
|
||||
end_container_children = []
|
||||
|
||||
if self.vinyl:
|
||||
end_container_children.append(self.vinyl)
|
||||
|
||||
end_container_children.append(self.status_container)
|
||||
if 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)
|
||||
|
||||
center_children = []
|
||||
if WINDOW_TITLE["enable"]:
|
||||
center_children.append(self.active_window)
|
||||
|
||||
self.children = CenterBox(
|
||||
name="bar-inner",
|
||||
start_children=Box(
|
||||
@@ -105,7 +136,7 @@ class StatusBar(Window):
|
||||
spacing=6,
|
||||
orientation="h",
|
||||
children=[
|
||||
Label(name="nixos-label", markup=""),
|
||||
Image(name="nixos-label", icon_name="nix-snowflake-white", icon_size=20),
|
||||
self.workspaces,
|
||||
],
|
||||
),
|
||||
@@ -113,7 +144,7 @@ class StatusBar(Window):
|
||||
name="center-container",
|
||||
spacing=4,
|
||||
orientation="h",
|
||||
children=[self.active_window],
|
||||
children=center_children,
|
||||
),
|
||||
end_children=Box(
|
||||
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()
|
||||
|
||||
def update_progress_bars(self):
|
||||
self.ram_progress_bar.value = psutil.virtual_memory().percent / 100
|
||||
self.cpu_progress_bar.value = psutil.cpu_percent() / 100
|
||||
return True
|
||||
def __del__(self):
|
||||
"""Cleanup when bar is destroyed"""
|
||||
if hasattr(self, 'calendar_service'):
|
||||
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
|
||||
import bar.modules.icons as icons
|
||||
from bar.services.mpris import MprisPlayerManager, MprisPlayer
|
||||
from fabric import Fabricator
|
||||
|
||||
# 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):
|
||||
if player_name:
|
||||
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.label import Label
|
||||
from fabric.widgets.eventbox import EventBox
|
||||
from fabric.widgets.overlay import Overlay
|
||||
from fabric.widgets.button import Button
|
||||
from fabric.widgets.image import Image
|
||||
from fabric.core.service import Property
|
||||
import subprocess
|
||||
|
||||
|
||||
class VinylButton(Box):
|
||||
class VinylButton(Button):
|
||||
@Property(bool, "read-write", default_value=False)
|
||||
def active(self) -> bool:
|
||||
return self._active
|
||||
@@ -25,41 +23,36 @@ class VinylButton(Box):
|
||||
|
||||
def __init__(
|
||||
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
|
||||
pw-link 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 """,
|
||||
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",
|
||||
"pw-link 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,
|
||||
):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
# Initialize properties
|
||||
self._active = False
|
||||
self._active_command = active_command
|
||||
self._inactive_command = inactive_command
|
||||
|
||||
# Set up the icon
|
||||
self.icon = Label(
|
||||
label="", # CD icon
|
||||
# Set up the icon using GTK icon
|
||||
self.icon = Image(
|
||||
icon_name="folder-music-symbolic",
|
||||
icon_size=16,
|
||||
name="vinyl-icon",
|
||||
style="",
|
||||
)
|
||||
|
||||
# Set up event box to handle clicks
|
||||
self.event_box = EventBox(
|
||||
events="button-press",
|
||||
child=Overlay(
|
||||
child=self.icon,
|
||||
),
|
||||
# Initialize the Button with the icon as child
|
||||
super().__init__(
|
||||
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
|
||||
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:
|
||||
self.remove_style_class("active")
|
||||
|
||||
def _on_clicked(self, _, event):
|
||||
def _on_clicked(self, button=None):
|
||||
"""Handle button click event"""
|
||||
if event.button == 1: # Left click
|
||||
# Toggle active state
|
||||
self.active = not self.active
|
||||
return True
|
||||
|
||||
def _execute_active_command(self):
|
||||
"""Execute shell command when button is activated"""
|
||||
try:
|
||||
subprocess.Popen(self._active_command, shell=True)
|
||||
for cmd in self._active_command:
|
||||
subprocess.Popen(cmd, shell=True)
|
||||
except Exception as e:
|
||||
print(f"Error executing active command: {e}")
|
||||
|
||||
def _execute_inactive_command(self):
|
||||
"""Execute shell command when button is deactivated"""
|
||||
try:
|
||||
subprocess.Popen(self._inactive_command, shell=True)
|
||||
for cmd in self._inactive_command:
|
||||
subprocess.Popen(cmd, shell=True)
|
||||
except Exception as e:
|
||||
print(f"Error executing inactive command: {e}")
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import operator
|
||||
from fabric.i3 import I3, I3MessageType
|
||||
from fabric.widgets.wayland import WaylandWindow as Window
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.label import Label
|
||||
from fabric.widgets.entry import Entry
|
||||
from gi.repository import Gdk
|
||||
from bar.services.wlr.service import WaylandWindowTracker, Window as WaylandWindow
|
||||
from pywayland.client import Display
|
||||
from bar.services.fenster import get_i3_connection
|
||||
|
||||
|
||||
class FuzzyWindowFinder(Window):
|
||||
@@ -21,9 +20,10 @@ class FuzzyWindowFinder(Window):
|
||||
type="popup",
|
||||
visible=False,
|
||||
)
|
||||
self.window_tracker = WaylandWindowTracker()
|
||||
self.window_tracker.ready_signal.connect(lambda *_: print("Tracker is ready"))
|
||||
self._all_windows: list[WaylandWindow] = []
|
||||
|
||||
self._i3 = get_i3_connection()
|
||||
self._all_windows = []
|
||||
self._refresh_windows()
|
||||
|
||||
self.viewport = Box(name="viewport", spacing=4, orientation="v")
|
||||
|
||||
@@ -48,41 +48,81 @@ class FuzzyWindowFinder(Window):
|
||||
self.add(self.picker_box)
|
||||
self.arrange_viewport("")
|
||||
|
||||
def open(self):
|
||||
self._all_windows = self.window_tracker.windows
|
||||
print(self._all_windows[0])
|
||||
self.arrange_viewport("")
|
||||
self.show()
|
||||
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, *_):
|
||||
text = entry.get_text()
|
||||
self.arrange_viewport(text) # Update list on typing
|
||||
print(text)
|
||||
self.arrange_viewport(text)
|
||||
|
||||
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 == Gdk.KEY_Return:
|
||||
self.window_tracker.activate_window(self._filtered[0])
|
||||
if event.keyval in [Gdk.KEY_Escape, 103]:
|
||||
self.hide()
|
||||
return True
|
||||
return False
|
||||
|
||||
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 = ""):
|
||||
self.viewport.children = [] # Clear previous entries
|
||||
|
||||
self._filtered = [
|
||||
w for w in self._all_windows if query.lower() in w.title.lower()
|
||||
]
|
||||
titles = [w.title for w in self._filtered]
|
||||
filtered = self._filter_windows(query)
|
||||
|
||||
for window in titles:
|
||||
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(
|
||||
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
|
||||
@@ -1,3 +0,0 @@
|
||||
from .service import River, RiverEvent
|
||||
|
||||
__all__ = ["River", "RiverEvent"]
|
||||
@@ -1,18 +0,0 @@
|
||||
# This file has been autogenerated by the pywayland scanner
|
||||
|
||||
# Copyright 2020 The River Developers
|
||||
#
|
||||
# Permission to use, copy, modify, and/or distribute this software for any
|
||||
# purpose with or without fee is hereby granted, provided that the above
|
||||
# copyright notice and this permission notice appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
from .zriver_command_callback_v1 import ZriverCommandCallbackV1 # noqa: F401
|
||||
from .zriver_control_v1 import ZriverControlV1 # noqa: F401
|
||||
@@ -1,84 +0,0 @@
|
||||
# This file has been autogenerated by the pywayland scanner
|
||||
|
||||
# Copyright 2020 The River Developers
|
||||
#
|
||||
# Permission to use, copy, modify, and/or distribute this software for any
|
||||
# purpose with or without fee is hereby granted, provided that the above
|
||||
# copyright notice and this permission notice appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pywayland.protocol_core import (Argument, ArgumentType, Global, Interface,
|
||||
Proxy, Resource)
|
||||
|
||||
|
||||
class ZriverCommandCallbackV1(Interface):
|
||||
"""Callback object
|
||||
|
||||
This object is created by the run_command request. Exactly one of the
|
||||
success or failure events will be sent. This object will be destroyed by
|
||||
the compositor after one of the events is sent.
|
||||
"""
|
||||
|
||||
name = "zriver_command_callback_v1"
|
||||
version = 1
|
||||
|
||||
|
||||
class ZriverCommandCallbackV1Proxy(Proxy[ZriverCommandCallbackV1]):
|
||||
interface = ZriverCommandCallbackV1
|
||||
|
||||
|
||||
class ZriverCommandCallbackV1Resource(Resource):
|
||||
interface = ZriverCommandCallbackV1
|
||||
|
||||
@ZriverCommandCallbackV1.event(
|
||||
Argument(ArgumentType.String),
|
||||
)
|
||||
def success(self, output: str) -> None:
|
||||
"""Command successful
|
||||
|
||||
Sent when the command has been successfully received and executed by
|
||||
the compositor. Some commands may produce output, in which case the
|
||||
output argument will be a non-empty string.
|
||||
|
||||
:param output:
|
||||
the output of the command
|
||||
:type output:
|
||||
`ArgumentType.String`
|
||||
"""
|
||||
self._post_event(0, output)
|
||||
|
||||
@ZriverCommandCallbackV1.event(
|
||||
Argument(ArgumentType.String),
|
||||
)
|
||||
def failure(self, failure_message: str) -> None:
|
||||
"""Command failed
|
||||
|
||||
Sent when the command could not be carried out. This could be due to
|
||||
sending a non-existent command, no command, not enough arguments, too
|
||||
many arguments, invalid arguments, etc.
|
||||
|
||||
:param failure_message:
|
||||
a message explaining why failure occurred
|
||||
:type failure_message:
|
||||
`ArgumentType.String`
|
||||
"""
|
||||
self._post_event(1, failure_message)
|
||||
|
||||
|
||||
class ZriverCommandCallbackV1Global(Global):
|
||||
interface = ZriverCommandCallbackV1
|
||||
|
||||
|
||||
ZriverCommandCallbackV1._gen_c()
|
||||
ZriverCommandCallbackV1.proxy_class = ZriverCommandCallbackV1Proxy
|
||||
ZriverCommandCallbackV1.resource_class = ZriverCommandCallbackV1Resource
|
||||
ZriverCommandCallbackV1.global_class = ZriverCommandCallbackV1Global
|
||||
@@ -1,111 +0,0 @@
|
||||
# This file has been autogenerated by the pywayland scanner
|
||||
|
||||
# Copyright 2020 The River Developers
|
||||
#
|
||||
# Permission to use, copy, modify, and/or distribute this software for any
|
||||
# purpose with or without fee is hereby granted, provided that the above
|
||||
# copyright notice and this permission notice appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pywayland.protocol_core import (
|
||||
Argument,
|
||||
ArgumentType,
|
||||
Global,
|
||||
Interface,
|
||||
Proxy,
|
||||
Resource,
|
||||
)
|
||||
|
||||
from pywayland.protocol.wayland import WlSeat
|
||||
from .zriver_command_callback_v1 import ZriverCommandCallbackV1
|
||||
|
||||
|
||||
class ZriverControlV1(Interface):
|
||||
"""Run compositor commands
|
||||
|
||||
This interface allows clients to run compositor commands and receive a
|
||||
success/failure response with output or a failure message respectively.
|
||||
|
||||
Each command is built up in a series of add_argument requests and executed
|
||||
with a run_command request. The first argument is the command to be run.
|
||||
|
||||
A complete list of commands should be made available in the man page of the
|
||||
compositor.
|
||||
"""
|
||||
|
||||
name = "zriver_control_v1"
|
||||
version = 1
|
||||
|
||||
|
||||
class ZriverControlV1Proxy(Proxy[ZriverControlV1]):
|
||||
interface = ZriverControlV1
|
||||
|
||||
@ZriverControlV1.request()
|
||||
def destroy(self) -> None:
|
||||
"""Destroy the river_control object
|
||||
|
||||
This request indicates that the client will not use the river_control
|
||||
object any more. Objects that have been created through this instance
|
||||
are not affected.
|
||||
"""
|
||||
self._marshal(0)
|
||||
self._destroy()
|
||||
|
||||
@ZriverControlV1.request(
|
||||
Argument(ArgumentType.String),
|
||||
)
|
||||
def add_argument(self, argument: str) -> None:
|
||||
"""Add an argument to the current command
|
||||
|
||||
Arguments are stored by the server in the order they were sent until
|
||||
the run_command request is made.
|
||||
|
||||
:param argument:
|
||||
the argument to add
|
||||
:type argument:
|
||||
`ArgumentType.String`
|
||||
"""
|
||||
self._marshal(1, argument)
|
||||
|
||||
@ZriverControlV1.request(
|
||||
Argument(ArgumentType.Object, interface=WlSeat),
|
||||
Argument(ArgumentType.NewId, interface=ZriverCommandCallbackV1),
|
||||
)
|
||||
def run_command(self, seat: WlSeat) -> Proxy[ZriverCommandCallbackV1]:
|
||||
"""Run the current command
|
||||
|
||||
Execute the command built up using the add_argument request for the
|
||||
given seat.
|
||||
|
||||
:param seat:
|
||||
:type seat:
|
||||
:class:`~pywayland.protocol.wayland.WlSeat`
|
||||
:returns:
|
||||
:class:`~pywayland.protocol.river_control_unstable_v1.ZriverCommandCallbackV1`
|
||||
-- callback object
|
||||
"""
|
||||
callback = self._marshal_constructor(2, ZriverCommandCallbackV1, seat)
|
||||
return callback
|
||||
|
||||
|
||||
class ZriverControlV1Resource(Resource):
|
||||
interface = ZriverControlV1
|
||||
|
||||
|
||||
class ZriverControlV1Global(Global):
|
||||
interface = ZriverControlV1
|
||||
|
||||
|
||||
ZriverControlV1._gen_c()
|
||||
ZriverControlV1.proxy_class = ZriverControlV1Proxy
|
||||
ZriverControlV1.resource_class = ZriverControlV1Resource
|
||||
ZriverControlV1.global_class = ZriverControlV1Global
|
||||
@@ -1,19 +0,0 @@
|
||||
# This file has been autogenerated by the pywayland scanner
|
||||
|
||||
# Copyright 2020 The River Developers
|
||||
#
|
||||
# Permission to use, copy, modify, and/or distribute this software for any
|
||||
# purpose with or without fee is hereby granted, provided that the above
|
||||
# copyright notice and this permission notice appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
from .zriver_output_status_v1 import ZriverOutputStatusV1 # noqa: F401
|
||||
from .zriver_seat_status_v1 import ZriverSeatStatusV1 # noqa: F401
|
||||
from .zriver_status_manager_v1 import ZriverStatusManagerV1 # noqa: F401
|
||||
@@ -1,134 +0,0 @@
|
||||
# This file has been autogenerated by the pywayland scanner
|
||||
|
||||
# Copyright 2020 The River Developers
|
||||
#
|
||||
# Permission to use, copy, modify, and/or distribute this software for any
|
||||
# purpose with or without fee is hereby granted, provided that the above
|
||||
# copyright notice and this permission notice appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pywayland.protocol_core import (Argument, ArgumentType, Global, Interface,
|
||||
Proxy, Resource)
|
||||
|
||||
|
||||
class ZriverOutputStatusV1(Interface):
|
||||
"""Track output tags and focus
|
||||
|
||||
This interface allows clients to receive information about the current
|
||||
windowing state of an output.
|
||||
"""
|
||||
|
||||
name = "zriver_output_status_v1"
|
||||
version = 4
|
||||
|
||||
|
||||
class ZriverOutputStatusV1Proxy(Proxy[ZriverOutputStatusV1]):
|
||||
interface = ZriverOutputStatusV1
|
||||
|
||||
@ZriverOutputStatusV1.request()
|
||||
def destroy(self) -> None:
|
||||
"""Destroy the river_output_status object
|
||||
|
||||
This request indicates that the client will not use the
|
||||
river_output_status object any more.
|
||||
"""
|
||||
self._marshal(0)
|
||||
self._destroy()
|
||||
|
||||
|
||||
class ZriverOutputStatusV1Resource(Resource):
|
||||
interface = ZriverOutputStatusV1
|
||||
|
||||
@ZriverOutputStatusV1.event(
|
||||
Argument(ArgumentType.Uint),
|
||||
)
|
||||
def focused_tags(self, tags: int) -> None:
|
||||
"""Focused tags of the output
|
||||
|
||||
Sent once binding the interface and again whenever the tag focus of the
|
||||
output changes.
|
||||
|
||||
:param tags:
|
||||
32-bit bitfield
|
||||
:type tags:
|
||||
`ArgumentType.Uint`
|
||||
"""
|
||||
self._post_event(0, tags)
|
||||
|
||||
@ZriverOutputStatusV1.event(
|
||||
Argument(ArgumentType.Array),
|
||||
)
|
||||
def view_tags(self, tags: list) -> None:
|
||||
"""Tag state of an output's views
|
||||
|
||||
Sent once on binding the interface and again whenever the tag state of
|
||||
the output changes.
|
||||
|
||||
:param tags:
|
||||
array of 32-bit bitfields
|
||||
:type tags:
|
||||
`ArgumentType.Array`
|
||||
"""
|
||||
self._post_event(1, tags)
|
||||
|
||||
@ZriverOutputStatusV1.event(
|
||||
Argument(ArgumentType.Uint),
|
||||
version=2,
|
||||
)
|
||||
def urgent_tags(self, tags: int) -> None:
|
||||
"""Tags of the output with an urgent view
|
||||
|
||||
Sent once on binding the interface and again whenever the set of tags
|
||||
with at least one urgent view changes.
|
||||
|
||||
:param tags:
|
||||
32-bit bitfield
|
||||
:type tags:
|
||||
`ArgumentType.Uint`
|
||||
"""
|
||||
self._post_event(2, tags)
|
||||
|
||||
@ZriverOutputStatusV1.event(
|
||||
Argument(ArgumentType.String),
|
||||
version=4,
|
||||
)
|
||||
def layout_name(self, name: str) -> None:
|
||||
"""Name of the layout
|
||||
|
||||
Sent once on binding the interface should a layout name exist and again
|
||||
whenever the name changes.
|
||||
|
||||
:param name:
|
||||
layout name
|
||||
:type name:
|
||||
`ArgumentType.String`
|
||||
"""
|
||||
self._post_event(3, name)
|
||||
|
||||
@ZriverOutputStatusV1.event(version=4)
|
||||
def layout_name_clear(self) -> None:
|
||||
"""Name of the layout
|
||||
|
||||
Sent when the current layout name has been removed without a new one
|
||||
being set, for example when the active layout generator disconnects.
|
||||
"""
|
||||
self._post_event(4)
|
||||
|
||||
|
||||
class ZriverOutputStatusV1Global(Global):
|
||||
interface = ZriverOutputStatusV1
|
||||
|
||||
|
||||
ZriverOutputStatusV1._gen_c()
|
||||
ZriverOutputStatusV1.proxy_class = ZriverOutputStatusV1Proxy
|
||||
ZriverOutputStatusV1.resource_class = ZriverOutputStatusV1Resource
|
||||
ZriverOutputStatusV1.global_class = ZriverOutputStatusV1Global
|
||||
@@ -1,124 +0,0 @@
|
||||
# This file has been autogenerated by the pywayland scanner
|
||||
|
||||
# Copyright 2020 The River Developers
|
||||
#
|
||||
# Permission to use, copy, modify, and/or distribute this software for any
|
||||
# purpose with or without fee is hereby granted, provided that the above
|
||||
# copyright notice and this permission notice appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pywayland.protocol.wayland import WlOutput
|
||||
from pywayland.protocol_core import (Argument, ArgumentType, Global, Interface,
|
||||
Proxy, Resource)
|
||||
|
||||
|
||||
class ZriverSeatStatusV1(Interface):
|
||||
"""Track seat focus
|
||||
|
||||
This interface allows clients to receive information about the current
|
||||
focus of a seat. Note that (un)focused_output events will only be sent if
|
||||
the client has bound the relevant
|
||||
:class:`~pywayland.protocol.wayland.WlOutput` globals.
|
||||
"""
|
||||
|
||||
name = "zriver_seat_status_v1"
|
||||
version = 3
|
||||
|
||||
|
||||
class ZriverSeatStatusV1Proxy(Proxy[ZriverSeatStatusV1]):
|
||||
interface = ZriverSeatStatusV1
|
||||
|
||||
@ZriverSeatStatusV1.request()
|
||||
def destroy(self) -> None:
|
||||
"""Destroy the river_seat_status object
|
||||
|
||||
This request indicates that the client will not use the
|
||||
river_seat_status object any more.
|
||||
"""
|
||||
self._marshal(0)
|
||||
self._destroy()
|
||||
|
||||
|
||||
class ZriverSeatStatusV1Resource(Resource):
|
||||
interface = ZriverSeatStatusV1
|
||||
|
||||
@ZriverSeatStatusV1.event(
|
||||
Argument(ArgumentType.Object, interface=WlOutput),
|
||||
)
|
||||
def focused_output(self, output: WlOutput) -> None:
|
||||
"""The seat focused an output
|
||||
|
||||
Sent on binding the interface and again whenever an output gains focus.
|
||||
|
||||
:param output:
|
||||
:type output:
|
||||
:class:`~pywayland.protocol.wayland.WlOutput`
|
||||
"""
|
||||
self._post_event(0, output)
|
||||
|
||||
@ZriverSeatStatusV1.event(
|
||||
Argument(ArgumentType.Object, interface=WlOutput),
|
||||
)
|
||||
def unfocused_output(self, output: WlOutput) -> None:
|
||||
"""The seat unfocused an output
|
||||
|
||||
Sent whenever an output loses focus.
|
||||
|
||||
:param output:
|
||||
:type output:
|
||||
:class:`~pywayland.protocol.wayland.WlOutput`
|
||||
"""
|
||||
self._post_event(1, output)
|
||||
|
||||
@ZriverSeatStatusV1.event(
|
||||
Argument(ArgumentType.String),
|
||||
)
|
||||
def focused_view(self, title: str) -> None:
|
||||
"""Information on the focused view
|
||||
|
||||
Sent once on binding the interface and again whenever the focused view
|
||||
or a property thereof changes. The title may be an empty string if no
|
||||
view is focused or the focused view did not set a title.
|
||||
|
||||
:param title:
|
||||
title of the focused view
|
||||
:type title:
|
||||
`ArgumentType.String`
|
||||
"""
|
||||
self._post_event(2, title)
|
||||
|
||||
@ZriverSeatStatusV1.event(
|
||||
Argument(ArgumentType.String),
|
||||
version=3,
|
||||
)
|
||||
def mode(self, name: str) -> None:
|
||||
"""The active mode changed
|
||||
|
||||
Sent once on binding the interface and again whenever a new mode is
|
||||
entered (e.g. with riverctl enter-mode foobar).
|
||||
|
||||
:param name:
|
||||
name of the mode
|
||||
:type name:
|
||||
`ArgumentType.String`
|
||||
"""
|
||||
self._post_event(3, name)
|
||||
|
||||
|
||||
class ZriverSeatStatusV1Global(Global):
|
||||
interface = ZriverSeatStatusV1
|
||||
|
||||
|
||||
ZriverSeatStatusV1._gen_c()
|
||||
ZriverSeatStatusV1.proxy_class = ZriverSeatStatusV1Proxy
|
||||
ZriverSeatStatusV1.resource_class = ZriverSeatStatusV1Resource
|
||||
ZriverSeatStatusV1.global_class = ZriverSeatStatusV1Global
|
||||
@@ -1,102 +0,0 @@
|
||||
# This file has been autogenerated by the pywayland scanner
|
||||
|
||||
# Copyright 2020 The River Developers
|
||||
#
|
||||
# Permission to use, copy, modify, and/or distribute this software for any
|
||||
# purpose with or without fee is hereby granted, provided that the above
|
||||
# copyright notice and this permission notice appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pywayland.protocol.wayland import WlOutput, WlSeat
|
||||
from pywayland.protocol_core import (Argument, ArgumentType, Global, Interface,
|
||||
Proxy, Resource)
|
||||
|
||||
from .zriver_output_status_v1 import ZriverOutputStatusV1
|
||||
from .zriver_seat_status_v1 import ZriverSeatStatusV1
|
||||
|
||||
|
||||
class ZriverStatusManagerV1(Interface):
|
||||
"""Manage river status objects
|
||||
|
||||
A global factory for objects that receive status information specific to
|
||||
river. It could be used to implement, for example, a status bar.
|
||||
"""
|
||||
|
||||
name = "zriver_status_manager_v1"
|
||||
version = 4
|
||||
|
||||
|
||||
class ZriverStatusManagerV1Proxy(Proxy[ZriverStatusManagerV1]):
|
||||
interface = ZriverStatusManagerV1
|
||||
|
||||
@ZriverStatusManagerV1.request()
|
||||
def destroy(self) -> None:
|
||||
"""Destroy the river_status_manager object
|
||||
|
||||
This request indicates that the client will not use the
|
||||
river_status_manager object any more. Objects that have been created
|
||||
through this instance are not affected.
|
||||
"""
|
||||
self._marshal(0)
|
||||
self._destroy()
|
||||
|
||||
@ZriverStatusManagerV1.request(
|
||||
Argument(ArgumentType.NewId, interface=ZriverOutputStatusV1),
|
||||
Argument(ArgumentType.Object, interface=WlOutput),
|
||||
)
|
||||
def get_river_output_status(self, output: WlOutput) -> Proxy[ZriverOutputStatusV1]:
|
||||
"""Create an output status object
|
||||
|
||||
This creates a new river_output_status object for the given
|
||||
:class:`~pywayland.protocol.wayland.WlOutput`.
|
||||
|
||||
:param output:
|
||||
:type output:
|
||||
:class:`~pywayland.protocol.wayland.WlOutput`
|
||||
:returns:
|
||||
:class:`~pywayland.protocol.river_status_unstable_v1.ZriverOutputStatusV1`
|
||||
"""
|
||||
id = self._marshal_constructor(1, ZriverOutputStatusV1, output)
|
||||
return id
|
||||
|
||||
@ZriverStatusManagerV1.request(
|
||||
Argument(ArgumentType.NewId, interface=ZriverSeatStatusV1),
|
||||
Argument(ArgumentType.Object, interface=WlSeat),
|
||||
)
|
||||
def get_river_seat_status(self, seat: WlSeat) -> Proxy[ZriverSeatStatusV1]:
|
||||
"""Create a seat status object
|
||||
|
||||
This creates a new river_seat_status object for the given
|
||||
:class:`~pywayland.protocol.wayland.WlSeat`.
|
||||
|
||||
:param seat:
|
||||
:type seat:
|
||||
:class:`~pywayland.protocol.wayland.WlSeat`
|
||||
:returns:
|
||||
:class:`~pywayland.protocol.river_status_unstable_v1.ZriverSeatStatusV1`
|
||||
"""
|
||||
id = self._marshal_constructor(2, ZriverSeatStatusV1, seat)
|
||||
return id
|
||||
|
||||
|
||||
class ZriverStatusManagerV1Resource(Resource):
|
||||
interface = ZriverStatusManagerV1
|
||||
|
||||
|
||||
class ZriverStatusManagerV1Global(Global):
|
||||
interface = ZriverStatusManagerV1
|
||||
|
||||
|
||||
ZriverStatusManagerV1._gen_c()
|
||||
ZriverStatusManagerV1.proxy_class = ZriverStatusManagerV1Proxy
|
||||
ZriverStatusManagerV1.resource_class = ZriverStatusManagerV1Resource
|
||||
ZriverStatusManagerV1.global_class = ZriverStatusManagerV1Global
|
||||
@@ -1,85 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<protocol name="river_control_unstable_v1">
|
||||
<copyright>
|
||||
Copyright 2020 The River Developers
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
</copyright>
|
||||
|
||||
<interface name="zriver_control_v1" version="1">
|
||||
<description summary="run compositor commands">
|
||||
This interface allows clients to run compositor commands and receive a
|
||||
success/failure response with output or a failure message respectively.
|
||||
|
||||
Each command is built up in a series of add_argument requests and
|
||||
executed with a run_command request. The first argument is the command
|
||||
to be run.
|
||||
|
||||
A complete list of commands should be made available in the man page of
|
||||
the compositor.
|
||||
</description>
|
||||
|
||||
<request name="destroy" type="destructor">
|
||||
<description summary="destroy the river_control object">
|
||||
This request indicates that the client will not use the
|
||||
river_control object any more. Objects that have been created
|
||||
through this instance are not affected.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<request name="add_argument">
|
||||
<description summary="add an argument to the current command">
|
||||
Arguments are stored by the server in the order they were sent until
|
||||
the run_command request is made.
|
||||
</description>
|
||||
<arg name="argument" type="string" summary="the argument to add"/>
|
||||
</request>
|
||||
|
||||
<request name="run_command">
|
||||
<description summary="run the current command">
|
||||
Execute the command built up using the add_argument request for the
|
||||
given seat.
|
||||
</description>
|
||||
<arg name="seat" type="object" interface="wl_seat"/>
|
||||
<arg name="callback" type="new_id" interface="zriver_command_callback_v1"
|
||||
summary="callback object"/>
|
||||
</request>
|
||||
</interface>
|
||||
|
||||
<interface name="zriver_command_callback_v1" version="1">
|
||||
<description summary="callback object">
|
||||
This object is created by the run_command request. Exactly one of the
|
||||
success or failure events will be sent. This object will be destroyed
|
||||
by the compositor after one of the events is sent.
|
||||
</description>
|
||||
|
||||
<event name="success" type="destructor">
|
||||
<description summary="command successful">
|
||||
Sent when the command has been successfully received and executed by
|
||||
the compositor. Some commands may produce output, in which case the
|
||||
output argument will be a non-empty string.
|
||||
</description>
|
||||
<arg name="output" type="string" summary="the output of the command"/>
|
||||
</event>
|
||||
|
||||
<event name="failure" type="destructor">
|
||||
<description summary="command failed">
|
||||
Sent when the command could not be carried out. This could be due to
|
||||
sending a non-existent command, no command, not enough arguments, too
|
||||
many arguments, invalid arguments, etc.
|
||||
</description>
|
||||
<arg name="failure_message" type="string"
|
||||
summary="a message explaining why failure occurred"/>
|
||||
</event>
|
||||
</interface>
|
||||
</protocol>
|
||||
@@ -1,148 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<protocol name="river_status_unstable_v1">
|
||||
<copyright>
|
||||
Copyright 2020 The River Developers
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
</copyright>
|
||||
|
||||
<interface name="zriver_status_manager_v1" version="4">
|
||||
<description summary="manage river status objects">
|
||||
A global factory for objects that receive status information specific
|
||||
to river. It could be used to implement, for example, a status bar.
|
||||
</description>
|
||||
|
||||
<request name="destroy" type="destructor">
|
||||
<description summary="destroy the river_status_manager object">
|
||||
This request indicates that the client will not use the
|
||||
river_status_manager object any more. Objects that have been created
|
||||
through this instance are not affected.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<request name="get_river_output_status">
|
||||
<description summary="create an output status object">
|
||||
This creates a new river_output_status object for the given wl_output.
|
||||
</description>
|
||||
<arg name="id" type="new_id" interface="zriver_output_status_v1"/>
|
||||
<arg name="output" type="object" interface="wl_output"/>
|
||||
</request>
|
||||
|
||||
<request name="get_river_seat_status">
|
||||
<description summary="create a seat status object">
|
||||
This creates a new river_seat_status object for the given wl_seat.
|
||||
</description>
|
||||
<arg name="id" type="new_id" interface="zriver_seat_status_v1"/>
|
||||
<arg name="seat" type="object" interface="wl_seat"/>
|
||||
</request>
|
||||
</interface>
|
||||
|
||||
<interface name="zriver_output_status_v1" version="4">
|
||||
<description summary="track output tags and focus">
|
||||
This interface allows clients to receive information about the current
|
||||
windowing state of an output.
|
||||
</description>
|
||||
|
||||
<request name="destroy" type="destructor">
|
||||
<description summary="destroy the river_output_status object">
|
||||
This request indicates that the client will not use the
|
||||
river_output_status object any more.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<event name="focused_tags">
|
||||
<description summary="focused tags of the output">
|
||||
Sent once binding the interface and again whenever the tag focus of
|
||||
the output changes.
|
||||
</description>
|
||||
<arg name="tags" type="uint" summary="32-bit bitfield"/>
|
||||
</event>
|
||||
|
||||
<event name="view_tags">
|
||||
<description summary="tag state of an output's views">
|
||||
Sent once on binding the interface and again whenever the tag state
|
||||
of the output changes.
|
||||
</description>
|
||||
<arg name="tags" type="array" summary="array of 32-bit bitfields"/>
|
||||
</event>
|
||||
|
||||
<event name="urgent_tags" since="2">
|
||||
<description summary="tags of the output with an urgent view">
|
||||
Sent once on binding the interface and again whenever the set of
|
||||
tags with at least one urgent view changes.
|
||||
</description>
|
||||
<arg name="tags" type="uint" summary="32-bit bitfield"/>
|
||||
</event>
|
||||
|
||||
<event name="layout_name" since="4">
|
||||
<description summary="name of the layout">
|
||||
Sent once on binding the interface should a layout name exist and again
|
||||
whenever the name changes.
|
||||
</description>
|
||||
<arg name="name" type="string" summary="layout name"/>
|
||||
</event>
|
||||
|
||||
<event name="layout_name_clear" since="4">
|
||||
<description summary="name of the layout">
|
||||
Sent when the current layout name has been removed without a new one
|
||||
being set, for example when the active layout generator disconnects.
|
||||
</description>
|
||||
</event>
|
||||
</interface>
|
||||
|
||||
<interface name="zriver_seat_status_v1" version="3">
|
||||
<description summary="track seat focus">
|
||||
This interface allows clients to receive information about the current
|
||||
focus of a seat. Note that (un)focused_output events will only be sent
|
||||
if the client has bound the relevant wl_output globals.
|
||||
</description>
|
||||
|
||||
<request name="destroy" type="destructor">
|
||||
<description summary="destroy the river_seat_status object">
|
||||
This request indicates that the client will not use the
|
||||
river_seat_status object any more.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<event name="focused_output">
|
||||
<description summary="the seat focused an output">
|
||||
Sent on binding the interface and again whenever an output gains focus.
|
||||
</description>
|
||||
<arg name="output" type="object" interface="wl_output"/>
|
||||
</event>
|
||||
|
||||
<event name="unfocused_output">
|
||||
<description summary="the seat unfocused an output">
|
||||
Sent whenever an output loses focus.
|
||||
</description>
|
||||
<arg name="output" type="object" interface="wl_output"/>
|
||||
</event>
|
||||
|
||||
<event name="focused_view">
|
||||
<description summary="information on the focused view">
|
||||
Sent once on binding the interface and again whenever the focused
|
||||
view or a property thereof changes. The title may be an empty string
|
||||
if no view is focused or the focused view did not set a title.
|
||||
</description>
|
||||
<arg name="title" type="string" summary="title of the focused view"/>
|
||||
</event>
|
||||
|
||||
<event name="mode" since="3">
|
||||
<description summary="the active mode changed">
|
||||
Sent once on binding the interface and again whenever a new mode
|
||||
is entered (e.g. with riverctl enter-mode foobar).
|
||||
</description>
|
||||
<arg name="name" type="string" summary="name of the mode"/>
|
||||
</event>
|
||||
</interface>
|
||||
</protocol>
|
||||
@@ -1,352 +0,0 @@
|
||||
import os
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, List, Optional, Set
|
||||
|
||||
from fabric.core.service import Property, Service, Signal
|
||||
from fabric.utils.helpers import idle_add
|
||||
from gi.repository import GLib
|
||||
from loguru import logger
|
||||
|
||||
# Import pywayland components - ensure these imports are correct
|
||||
from pywayland.client import Display
|
||||
from pywayland.protocol.wayland import WlOutput, WlSeat
|
||||
|
||||
from .protocols.generated.river_control_unstable_v1 import ZriverControlV1
|
||||
from .protocols.generated.river_status_unstable_v1 import ZriverStatusManagerV1
|
||||
|
||||
|
||||
@dataclass
|
||||
class OutputInfo:
|
||||
"""Information about a River output"""
|
||||
|
||||
name: int
|
||||
output: WlOutput
|
||||
status: Any = None # ZriverOutputStatusV1
|
||||
tags_view: List[int] = field(default_factory=list)
|
||||
tags_focused: List[int] = field(default_factory=list)
|
||||
tags_urgent: List[int] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RiverEvent:
|
||||
"""Event data from River compositor"""
|
||||
|
||||
name: str
|
||||
data: List[Any]
|
||||
output_id: Optional[int] = None
|
||||
|
||||
|
||||
class River(Service):
|
||||
"""Connection to River Wayland compositor via river-status protocol"""
|
||||
|
||||
@Property(bool, "readable", "is-ready", default_value=False)
|
||||
def ready(self) -> bool:
|
||||
return self._ready
|
||||
|
||||
@Property(str, "readable", "active-window", default_value="")
|
||||
def active_window(self) -> str:
|
||||
"""Get the title of the currently active window"""
|
||||
return self._active_window_title
|
||||
|
||||
@Signal
|
||||
def ready_signal(self):
|
||||
return self.notify("ready")
|
||||
|
||||
@Signal("event", flags="detailed")
|
||||
def event(self, event: object): ...
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the River service"""
|
||||
super().__init__(**kwargs)
|
||||
self._ready = False
|
||||
self._active_window_title = ""
|
||||
self.outputs: Dict[int, OutputInfo] = {}
|
||||
self._display = None
|
||||
self.river_status_mgr = None
|
||||
self.river_control = None
|
||||
self.seat = None
|
||||
self.seat_status = None
|
||||
|
||||
# Start the connection in a separate thread
|
||||
self.river_thread = GLib.Thread.new(
|
||||
"river-status-service", self._river_connection_task
|
||||
)
|
||||
|
||||
def _river_connection_task(self):
|
||||
"""Main thread that connects to River and listens for events"""
|
||||
try:
|
||||
logger.info("[RiverService] Starting connection to River")
|
||||
|
||||
logger.debug(
|
||||
f"[RiverService] XDG_RUNTIME_DIR={os.environ.get('XDG_RUNTIME_DIR', 'Not set')}"
|
||||
)
|
||||
logger.debug(
|
||||
f"[RiverService] WAYLAND_DISPLAY={os.environ.get('WAYLAND_DISPLAY', 'Not set')}"
|
||||
)
|
||||
|
||||
self._display = Display()
|
||||
self._display.connect()
|
||||
|
||||
# Get the registry
|
||||
registry = self._display.get_registry()
|
||||
logger.debug("[RiverService] Registry obtained")
|
||||
|
||||
# Create state object to hold our data
|
||||
state = {
|
||||
"display": self._display,
|
||||
"registry": registry,
|
||||
"outputs": {},
|
||||
"river_status_mgr": None,
|
||||
"river_control": None,
|
||||
"seat": None,
|
||||
"seat_status": None,
|
||||
}
|
||||
|
||||
def handle_global(registry, name, iface, version):
|
||||
logger.debug(
|
||||
f"[RiverService] Global: {iface} (v{version}, name={name})"
|
||||
)
|
||||
if iface == "zriver_status_manager_v1":
|
||||
state["river_status_mgr"] = registry.bind(
|
||||
name, ZriverStatusManagerV1, version
|
||||
)
|
||||
logger.info("[RiverService] Found river status manager")
|
||||
elif iface == "zriver_control_v1":
|
||||
state["river_control"] = registry.bind(
|
||||
name, ZriverControlV1, version
|
||||
)
|
||||
logger.info("[RiverService] Found river control interface")
|
||||
elif iface == "wl_output":
|
||||
output = registry.bind(name, WlOutput, version)
|
||||
state["outputs"][name] = OutputInfo(name=name, output=output)
|
||||
logger.info(f"[RiverService] Found output {name}")
|
||||
elif iface == "wl_seat":
|
||||
state["seat"] = registry.bind(name, WlSeat, version)
|
||||
logger.info("[RiverService] Found seat")
|
||||
|
||||
def handle_global_remove(registry, name):
|
||||
if name in state["outputs"]:
|
||||
logger.info(f"[RiverService] Output {name} removed")
|
||||
del state["outputs"][name]
|
||||
idle_add(
|
||||
lambda: self.emit(
|
||||
"event::output_removed",
|
||||
RiverEvent("output_removed", [name]),
|
||||
)
|
||||
)
|
||||
|
||||
# Set up the dispatchers
|
||||
registry.dispatcher["global"] = handle_global
|
||||
registry.dispatcher["global_remove"] = handle_global_remove
|
||||
|
||||
# Discover globals
|
||||
logger.debug("[RiverService] Performing initial roundtrip")
|
||||
self._display.roundtrip()
|
||||
|
||||
# Check if we found the river status manager
|
||||
if not state["river_status_mgr"]:
|
||||
logger.error("[RiverService] River status manager not found")
|
||||
return
|
||||
|
||||
# Handle the window title updates through seat status
|
||||
|
||||
if not state["river_control"]:
|
||||
logger.error(
|
||||
"[RiverService] River control interface not found - falling back to riverctl"
|
||||
)
|
||||
# You could still fall back to the old riverctl method here if needed
|
||||
|
||||
def focused_view_handler(_, title):
|
||||
logger.debug(f"[RiverService] Focused view title: {title}")
|
||||
self._active_window_title = title
|
||||
idle_add(lambda: self._emit_active_window(title))
|
||||
|
||||
# Get the seat status to track active window
|
||||
|
||||
if state["seat"]:
|
||||
seat_status = state["river_status_mgr"].get_river_seat_status(
|
||||
state["seat"]
|
||||
)
|
||||
seat_status.dispatcher["focused_view"] = focused_view_handler
|
||||
state["seat_status"] = seat_status
|
||||
logger.info("[RiverService] Set up seat status for window tracking")
|
||||
|
||||
# Create view tags and focused tags handlers
|
||||
def make_view_tags_handler(output_id):
|
||||
def handler(_, tags):
|
||||
decoded = self._decode_bitfields(tags)
|
||||
state["outputs"][output_id].tags_view = decoded
|
||||
logger.debug(
|
||||
f"[RiverService] Output {output_id} view tags: {decoded}"
|
||||
)
|
||||
idle_add(lambda: self._emit_view_tags(output_id, decoded))
|
||||
|
||||
return handler
|
||||
|
||||
def make_focused_tags_handler(output_id):
|
||||
def handler(_, tags):
|
||||
decoded = self._decode_bitfields(tags)
|
||||
state["outputs"][output_id].tags_focused = decoded
|
||||
logger.debug(
|
||||
f"[RiverService] Output {output_id} focused tags: {decoded}"
|
||||
)
|
||||
idle_add(lambda: self._emit_focused_tags(output_id, decoded))
|
||||
|
||||
return handler
|
||||
|
||||
def make_urgent_tags_handler(output_id):
|
||||
def handler(_, tags):
|
||||
decoded = self._decode_bitfields(tags)
|
||||
state["outputs"][output_id].tags_urgent = decoded
|
||||
logger.debug(
|
||||
f"[RiverService] Output {output_id} urgent tags: {decoded}"
|
||||
)
|
||||
idle_add(lambda: self._emit_urgent_tags(output_id, decoded))
|
||||
|
||||
return handler
|
||||
|
||||
# Bind output status listeners
|
||||
for name, info in list(state["outputs"].items()):
|
||||
status = state["river_status_mgr"].get_river_output_status(info.output)
|
||||
status.dispatcher["view_tags"] = make_view_tags_handler(name)
|
||||
status.dispatcher["focused_tags"] = make_focused_tags_handler(name)
|
||||
status.dispatcher["urgent_tags"] = make_urgent_tags_handler(name)
|
||||
info.status = status
|
||||
logger.info(f"[RiverService] Set up status for output {name}")
|
||||
|
||||
# Initial data fetch
|
||||
logger.debug("[RiverService] Performing second roundtrip")
|
||||
self._display.roundtrip()
|
||||
|
||||
# Update our outputs dictionary
|
||||
self.outputs.update(state["outputs"])
|
||||
self.river_status_mgr = state["river_status_mgr"]
|
||||
self.river_control = state["river_control"]
|
||||
self.seat = state["seat"]
|
||||
self.seat_status = state.get("seat_status")
|
||||
|
||||
# Mark service as ready
|
||||
idle_add(self._set_ready)
|
||||
|
||||
while True:
|
||||
self._display.dispatch(block=True)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[RiverService] Error in River connection: {e}")
|
||||
import traceback
|
||||
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
return True
|
||||
|
||||
def _set_ready(self):
|
||||
"""Set the service as ready (called on main thread via idle_add)"""
|
||||
self._ready = True
|
||||
logger.info("[RiverService] Service ready")
|
||||
self.ready_signal.emit()
|
||||
return False # Don't repeat
|
||||
|
||||
def _emit_view_tags(self, output_id, tags):
|
||||
"""Emit view_tags events (called on main thread)"""
|
||||
event = RiverEvent("view_tags", tags, output_id)
|
||||
self.emit("event::view_tags", event)
|
||||
self.emit(f"event::view_tags::{output_id}", tags)
|
||||
return False # Don't repeat
|
||||
|
||||
def _emit_focused_tags(self, output_id, tags):
|
||||
"""Emit focused_tags events (called on main thread)"""
|
||||
event = RiverEvent("focused_tags", tags, output_id)
|
||||
self.emit("event::focused_tags", event)
|
||||
self.emit(f"event::focused_tags::{output_id}", tags)
|
||||
return False # Don't repeat
|
||||
|
||||
def _emit_active_window(self, title):
|
||||
"""Emit active window title events (called on main thread)"""
|
||||
event = RiverEvent("active_window", [title])
|
||||
self.emit("event::active_window", event)
|
||||
self.notify("active-window")
|
||||
return False # Don't repeat
|
||||
|
||||
def _emit_urgent_tags(self, output_id, tags):
|
||||
"""Emit urgent_tags events (called on main thread)"""
|
||||
event = RiverEvent("urgent_tags", tags, output_id)
|
||||
self.emit("event::urgent_tags", event)
|
||||
self.emit(f"event::urgent_tags::{output_id}", tags)
|
||||
return False # Don't repeat
|
||||
|
||||
@staticmethod
|
||||
def _decode_bitfields(bitfields) -> List[int]:
|
||||
"""Decode River's tag bitfields into a list of tag indices"""
|
||||
tags: Set[int] = set()
|
||||
|
||||
# Ensure we have an iterable
|
||||
if not hasattr(bitfields, "__iter__"):
|
||||
bitfields = [bitfields]
|
||||
|
||||
for bits in bitfields:
|
||||
for i in range(32):
|
||||
if bits & (1 << i):
|
||||
tags.add(i)
|
||||
|
||||
return sorted(tags)
|
||||
|
||||
def run_command(self, command, *args, callback=None):
|
||||
"""Run a riverctl command"""
|
||||
if not self.river_control or not self.seat:
|
||||
logger.warning(
|
||||
"[RiverService] River control or seat not available, falling back to riverctl"
|
||||
)
|
||||
return self._run_command_fallback(command, *args)
|
||||
|
||||
self.river_control.add_argument(command)
|
||||
for arg in args:
|
||||
self.river_control.add_argument(str(arg))
|
||||
|
||||
# Execute the command
|
||||
command_callback = self.river_control.run_command(self.seat)
|
||||
|
||||
# Set up callback handlers
|
||||
result = {"stdout": None, "stderr": None, "success": None}
|
||||
|
||||
def handle_success(_, output):
|
||||
logger.debug(f"[RiverService] Command success: {output}")
|
||||
result["stdout"] = output
|
||||
result["success"] = True
|
||||
if callback:
|
||||
idle_add(lambda: callback(True, output, None))
|
||||
|
||||
def handle_failure(_, failure_message):
|
||||
logger.debug(f"[RiverService] Command failure: {failure_message}")
|
||||
result["stderr"] = failure_message
|
||||
result["success"] = False
|
||||
if callback:
|
||||
idle_add(lambda: callback(False, None, failure_message))
|
||||
|
||||
command_callback.dispatcher["success"] = handle_success
|
||||
command_callback.dispatcher["failure"] = handle_failure
|
||||
|
||||
if hasattr(self, "_display"):
|
||||
self._display.flush()
|
||||
|
||||
return True
|
||||
|
||||
def _run_command_fallback(self, command, *args):
|
||||
"""Fallback to riverctl"""
|
||||
import subprocess
|
||||
|
||||
cmd = ["riverctl", command] + [str(arg) for arg in args]
|
||||
try:
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
||||
logger.info(f"[RiverService] Ran command: {' '.join(cmd)}")
|
||||
return result.stdout.strip()
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(
|
||||
f"[RiverService] Command failed: {' '.join(cmd)}, error: {e.stderr}"
|
||||
)
|
||||
return None
|
||||
|
||||
def toggle_focused_tag(self, tag, callback=None):
|
||||
"""Toggle a tag in the focused tags"""
|
||||
tag_mask = 1 << int(tag)
|
||||
self.run_command("set-focused-tags", str(tag_mask), callback=callback)
|
||||
@@ -1,266 +0,0 @@
|
||||
from fabric.core.service import Property
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.button import Button
|
||||
from fabric.widgets.eventbox import EventBox
|
||||
from fabric.widgets.label import Label
|
||||
from gi.repository import Gdk
|
||||
from loguru import logger
|
||||
|
||||
from .service import River
|
||||
|
||||
connection: River | None = None
|
||||
|
||||
|
||||
def get_river_connection() -> River:
|
||||
global connection
|
||||
if not connection:
|
||||
connection = River()
|
||||
return connection
|
||||
|
||||
|
||||
class RiverWorkspaceButton(Button):
|
||||
@Property(int, "readable")
|
||||
def id(self) -> int:
|
||||
return self._id
|
||||
|
||||
@Property(bool, "read-write", default_value=False)
|
||||
def active(self) -> bool:
|
||||
return self._active
|
||||
|
||||
@active.setter
|
||||
def active(self, value: bool):
|
||||
self._active = value
|
||||
(self.remove_style_class if not value else self.add_style_class)("active")
|
||||
|
||||
@Property(bool, "read-write", default_value=False)
|
||||
def empty(self) -> bool:
|
||||
return self._empty
|
||||
|
||||
@empty.setter
|
||||
def empty(self, value: bool):
|
||||
self._empty = value
|
||||
(self.remove_style_class if not value else self.add_style_class)("empty")
|
||||
|
||||
@Property(bool, "read-write", default_value=False)
|
||||
def urgent(self) -> bool:
|
||||
return self._urgent
|
||||
|
||||
@urgent.setter
|
||||
def urgent(self, value: bool):
|
||||
self._urgent = value
|
||||
self._update_style()
|
||||
|
||||
def __init__(self, id: int, label: str = None, **kwargs):
|
||||
super().__init__(label or str(id), **kwargs)
|
||||
self._id = id
|
||||
self._active = False
|
||||
self._empty = True
|
||||
self._urgent = False
|
||||
|
||||
def _update_style(self):
|
||||
"""Update button styles based on states"""
|
||||
# Remove all state-related styles first
|
||||
self.remove_style_class("active")
|
||||
self.remove_style_class("empty")
|
||||
self.remove_style_class("urgent")
|
||||
|
||||
# Then apply current states
|
||||
if self._active:
|
||||
self.add_style_class("active")
|
||||
if self._empty:
|
||||
self.add_style_class("empty")
|
||||
if self._urgent:
|
||||
self.add_style_class("urgent")
|
||||
|
||||
|
||||
class RiverWorkspaces(EventBox):
|
||||
def __init__(self, output_id, river_service=None, max_tags=9, **kwargs):
|
||||
super().__init__(events="scroll")
|
||||
self._box = Box(**kwargs)
|
||||
self.children = self._box
|
||||
|
||||
if river_service:
|
||||
self.river = river_service
|
||||
|
||||
# Store output_id as received
|
||||
self.output_id = output_id
|
||||
|
||||
self.max_tags = max_tags
|
||||
# Create buttons for tags 0 to max_tags-1 (to match River's 0-based tag indexing)
|
||||
self._buttons = {i: RiverWorkspaceButton(i) for i in range(max_tags)}
|
||||
|
||||
for btn in self._buttons.values():
|
||||
btn.connect("clicked", self.on_workspace_click)
|
||||
self._box.add(btn)
|
||||
|
||||
# Connect to service events
|
||||
self.river.connect("event::focused_tags", self.on_focus_change_general)
|
||||
self.river.connect("event::view_tags", self.on_view_change_general)
|
||||
self.river.connect("event::urgent_tags", self.on_urgent_change_general)
|
||||
self.river.connect("event::output_removed", self.on_output_removed)
|
||||
|
||||
# Initial setup when service is ready
|
||||
if self.river.ready:
|
||||
self.on_ready(None)
|
||||
else:
|
||||
self.river.connect("event::ready", self.on_ready)
|
||||
|
||||
self.connect("scroll-event", self.on_scroll)
|
||||
|
||||
def on_ready(self, _):
|
||||
"""Initialize widget state when service is ready"""
|
||||
|
||||
if self.output_id is None and self.river.outputs:
|
||||
self.output_id = next(iter(self.river.outputs.keys()))
|
||||
logger.info(f"[RiverWorkspaces] Selected output {self.output_id}")
|
||||
|
||||
if self.output_id is not None and self.output_id in self.river.outputs:
|
||||
output_info = self.river.outputs[self.output_id]
|
||||
|
||||
focused_tags = output_info.tags_focused
|
||||
view_tags = output_info.tags_view
|
||||
urgent_tags = output_info.tags_urgent
|
||||
|
||||
for i, btn in self._buttons.items():
|
||||
btn.active = i in focused_tags
|
||||
btn.empty = i not in view_tags
|
||||
btn.urgent = i in urgent_tags
|
||||
|
||||
def on_focus_change(self, _, tags):
|
||||
"""Handle focused tags change for our specific output"""
|
||||
logger.info(
|
||||
f"[RiverWorkspaces] Focus change on output {self.output_id}: {tags}"
|
||||
)
|
||||
for i, btn in self._buttons.items():
|
||||
btn.active = i in tags
|
||||
|
||||
def on_view_change(self, _, tags):
|
||||
"""Handle view tags change for our specific output"""
|
||||
logger.info(f"[RiverWorkspaces] View change on output {self.output_id}: {tags}")
|
||||
for i, btn in self._buttons.items():
|
||||
btn.empty = i not in tags
|
||||
|
||||
def on_focus_change_general(self, _, event):
|
||||
"""Handle general focused tags event"""
|
||||
# Only handle event if it's for our output
|
||||
if event.output_id == self.output_id:
|
||||
logger.info(
|
||||
f"[RiverWorkspaces] General focus change for output {self.output_id}"
|
||||
)
|
||||
self.on_focus_change(_, event.data)
|
||||
|
||||
def on_view_change_general(self, _, event):
|
||||
"""Handle general view tags event"""
|
||||
# Only handle event if it's for our output
|
||||
if event.output_id == self.output_id:
|
||||
logger.info(
|
||||
f"[RiverWorkspaces] General view change for output {self.output_id}"
|
||||
)
|
||||
self.on_view_change(_, event.data)
|
||||
|
||||
def on_urgent_change(self, _, tags):
|
||||
"""Handle urgent tags change for our specific output"""
|
||||
logger.info(
|
||||
f"[RiverWorkspaces] Urgent change on output {self.output_id}: {tags}"
|
||||
)
|
||||
for i, btn in self._buttons.items():
|
||||
btn.urgent = i in tags
|
||||
|
||||
def on_urgent_change_general(self, _, event):
|
||||
"""Handle general urgent tags event"""
|
||||
# Only handle event if it's for our output
|
||||
if event.output_id == self.output_id:
|
||||
logger.info(
|
||||
f"[RiverWorkspaces] General urgent change for output {self.output_id}"
|
||||
)
|
||||
self.on_urgent_change(_, event.data)
|
||||
|
||||
def on_output_removed(self, _, event):
|
||||
"""Handle output removal"""
|
||||
removed_id = event.data[0]
|
||||
|
||||
if removed_id == self.output_id:
|
||||
logger.info(f"[RiverWorkspaces] Our output {self.output_id} was removed")
|
||||
|
||||
# Try to find another output
|
||||
if self.river.outputs:
|
||||
self.output_id = next(iter(self.river.outputs.keys()))
|
||||
logger.info(f"[RiverWorkspaces] Switching to output {self.output_id}")
|
||||
|
||||
# Update state for new output
|
||||
if self.output_id in self.river.outputs:
|
||||
output_info = self.river.outputs[self.output_id]
|
||||
# Access fields directly on the OutputInfo dataclass
|
||||
focused_tags = output_info.tags_focused
|
||||
view_tags = output_info.tags_view
|
||||
|
||||
for i, btn in self._buttons.items():
|
||||
btn.active = i in focused_tags
|
||||
btn.empty = i not in view_tags
|
||||
|
||||
def on_workspace_click(self, btn):
|
||||
"""Handle workspace button click"""
|
||||
logger.info(f"[RiverWorkspaces] Clicked on workspace {btn.id}")
|
||||
self.river.toggle_focused_tag(btn.id)
|
||||
|
||||
def on_scroll(self, _, event):
|
||||
"""Handle scroll events"""
|
||||
direction = event.direction
|
||||
if direction == Gdk.ScrollDirection.DOWN:
|
||||
logger.info("[RiverWorkspaces] Scroll down - focusing next view")
|
||||
self.river.run_command("focus-view", "next")
|
||||
elif direction == Gdk.ScrollDirection.UP:
|
||||
logger.info("[RiverWorkspaces] Scroll up - focusing previous view")
|
||||
self.river.run_command("focus-view", "previous")
|
||||
|
||||
|
||||
class RiverActiveWindow(Label):
|
||||
"""Widget to display the currently active window's title"""
|
||||
|
||||
def __init__(self, max_length=None, ellipsize="end", river_service=None, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
if river_service:
|
||||
self.river = river_service
|
||||
|
||||
self.max_length = max_length
|
||||
self.ellipsize = ellipsize
|
||||
|
||||
# Set initial state
|
||||
if self.river.ready:
|
||||
self.on_ready(None)
|
||||
else:
|
||||
self.river.connect("event::ready", self.on_ready)
|
||||
|
||||
# Connect to active window changes
|
||||
self.river.connect("event::active_window", self.on_active_window_changed)
|
||||
|
||||
def on_ready(self, _):
|
||||
"""Initialize widget when service is ready"""
|
||||
logger.info("[RiverActiveWindow] Connected to service")
|
||||
self.update_title(self.river.active_window)
|
||||
|
||||
def on_active_window_changed(self, _, event):
|
||||
"""Update widget when active window changes"""
|
||||
title = event.data[0] if event.data else ""
|
||||
logger.debug(f"[RiverActiveWindow] Window changed to: {title}")
|
||||
self.update_title(title)
|
||||
|
||||
def update_title(self, title):
|
||||
"""Update the label with the window title"""
|
||||
if not title:
|
||||
self.label = ""
|
||||
self.set_label(self.label)
|
||||
return
|
||||
|
||||
if self.max_length and len(title) > self.max_length:
|
||||
if self.ellipsize == "end":
|
||||
title = title[: self.max_length] + "..."
|
||||
elif self.ellipsize == "middle":
|
||||
half = (self.max_length - 3) // 2
|
||||
title = title[:half] + "..." + title[-half:]
|
||||
elif self.ellipsize == "start":
|
||||
title = "..." + title[-self.max_length :]
|
||||
|
||||
self.label = title
|
||||
self.set_label(self.label)
|
||||
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
|
||||
@@ -1,21 +0,0 @@
|
||||
from fabric.core.service import Service, Property
|
||||
from pywayland.client import Display
|
||||
from gi.repository import GLib
|
||||
|
||||
|
||||
class WaylandEventLoopService(Service):
|
||||
@Property(object, "readable", "display")
|
||||
def display_property(self):
|
||||
return self._display
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self._display = Display()
|
||||
self._display.connect()
|
||||
|
||||
self.thread = GLib.Thread.new("wayland-loop", self._loop)
|
||||
|
||||
def _loop(self):
|
||||
while True:
|
||||
self._display.dispatch(block=True)
|
||||
print("DISPATCHING...")
|
||||
@@ -1,233 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import sys
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
import pywayland
|
||||
from pywayland.client import Display
|
||||
from pywayland.protocol.wayland import WlOutput, WlSeat
|
||||
|
||||
# Import the protocol interfaces from your files
|
||||
from wlr_foreign_toplevel_management_unstable_v1.zwlr_foreign_toplevel_manager_v1 import (
|
||||
ZwlrForeignToplevelManagerV1,
|
||||
)
|
||||
from wlr_foreign_toplevel_management_unstable_v1.zwlr_foreign_toplevel_handle_v1 import (
|
||||
ZwlrForeignToplevelHandleV1,
|
||||
)
|
||||
|
||||
|
||||
class Window:
|
||||
"""Represents a toplevel window in the compositor."""
|
||||
|
||||
def __init__(self, handle: ZwlrForeignToplevelHandleV1):
|
||||
self.handle = handle
|
||||
self.title: str = "Unknown"
|
||||
self.app_id: str = "Unknown"
|
||||
self.states: List[str] = []
|
||||
self.outputs: List[WlOutput] = []
|
||||
self.parent: Optional["Window"] = None
|
||||
self.closed = False
|
||||
|
||||
def __str__(self) -> str:
|
||||
state_str = (
|
||||
", ".join([ZwlrForeignToplevelHandleV1.state(s).name for s in self.states])
|
||||
if self.states
|
||||
else "normal"
|
||||
)
|
||||
return (
|
||||
f"Window(title='{self.title}', app_id='{self.app_id}', state={state_str})"
|
||||
)
|
||||
|
||||
|
||||
class WaylandWindowManager:
|
||||
"""Manages Wayland windows using the foreign toplevel protocol."""
|
||||
|
||||
def __init__(self):
|
||||
self.display = Display()
|
||||
self.windows: Dict[ZwlrForeignToplevelHandleV1, Window] = {}
|
||||
self.manager = None
|
||||
self.running = False
|
||||
|
||||
def connect(self) -> bool:
|
||||
"""Connect to the Wayland display and bind to the toplevel manager."""
|
||||
try:
|
||||
self.display.connect()
|
||||
print("Connected to Wayland display")
|
||||
|
||||
# Get the registry to find the foreign toplevel manager
|
||||
registry = self.display.get_registry()
|
||||
registry.dispatcher["global"] = self._registry_global_handler
|
||||
|
||||
# Roundtrip to process registry events
|
||||
self.display.roundtrip()
|
||||
|
||||
if not self.manager:
|
||||
print(
|
||||
"Foreign toplevel manager not found. Is wlr-foreign-toplevel-management protocol supported?"
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Failed to connect: {e}")
|
||||
return False
|
||||
|
||||
def _registry_global_handler(self, registry, id, interface, version):
|
||||
"""Handle registry global objects."""
|
||||
if interface == ZwlrForeignToplevelManagerV1.name:
|
||||
print(f"Found foreign toplevel manager (id={id}, version={version})")
|
||||
self.manager = registry.bind(
|
||||
id, ZwlrForeignToplevelManagerV1, min(version, 3)
|
||||
)
|
||||
self.manager.dispatcher["toplevel"] = self._handle_toplevel
|
||||
self.manager.dispatcher["finished"] = self._handle_manager_finished
|
||||
|
||||
def _handle_toplevel(self, manager, toplevel):
|
||||
"""Handle a new toplevel window."""
|
||||
window = Window(toplevel)
|
||||
self.windows[toplevel] = window
|
||||
print(window)
|
||||
|
||||
# Setup event dispatchers for the toplevel
|
||||
toplevel.dispatcher["title"] = self._handle_title
|
||||
toplevel.dispatcher["app_id"] = self._handle_app_id
|
||||
toplevel.dispatcher["state"] = self._handle_state
|
||||
toplevel.dispatcher["done"] = self._handle_done
|
||||
toplevel.dispatcher["closed"] = self._handle_closed
|
||||
toplevel.dispatcher["output_enter"] = self._handle_output_enter
|
||||
toplevel.dispatcher["output_leave"] = self._handle_output_leave
|
||||
|
||||
def _handle_title(self, toplevel, title):
|
||||
"""Handle toplevel title changes."""
|
||||
window = self.windows.get(toplevel)
|
||||
if window:
|
||||
window.title = title
|
||||
|
||||
def _handle_app_id(self, toplevel, app_id):
|
||||
"""Handle toplevel app_id changes."""
|
||||
window = self.windows.get(toplevel)
|
||||
if window:
|
||||
window.app_id = app_id
|
||||
|
||||
def _handle_state(self, toplevel, states):
|
||||
"""Handle toplevel state changes."""
|
||||
window = self.windows.get(toplevel)
|
||||
if window:
|
||||
window.states = states
|
||||
|
||||
def _handle_done(self, toplevel):
|
||||
"""Handle toplevel done event."""
|
||||
window = self.windows.get(toplevel)
|
||||
if window and not window.closed:
|
||||
print(f"Window updated: {window}")
|
||||
|
||||
def _handle_closed(self, toplevel):
|
||||
"""Handle toplevel closed event."""
|
||||
window = self.windows.get(toplevel)
|
||||
if window:
|
||||
window.closed = True
|
||||
print(f"Window closed: {window}")
|
||||
# Clean up the toplevel object
|
||||
toplevel.destroy()
|
||||
# Remove from our dictionary
|
||||
del self.windows[toplevel]
|
||||
|
||||
def _handle_output_enter(self, toplevel, output):
|
||||
"""Handle toplevel entering an output."""
|
||||
window = self.windows.get(toplevel)
|
||||
if window and output not in window.outputs:
|
||||
window.outputs.append(output)
|
||||
|
||||
def _handle_output_leave(self, toplevel, output):
|
||||
"""Handle toplevel leaving an output."""
|
||||
window = self.windows.get(toplevel)
|
||||
if window and output in window.outputs:
|
||||
window.outputs.remove(output)
|
||||
|
||||
def _handle_parent(self, toplevel, parent):
|
||||
"""Handle toplevel parent changes."""
|
||||
window = self.windows.get(toplevel)
|
||||
if window:
|
||||
if parent is None:
|
||||
window.parent = None
|
||||
else:
|
||||
parent_window = self.windows.get(parent)
|
||||
if parent_window:
|
||||
window.parent = parent_window
|
||||
|
||||
def _handle_manager_finished(self, manager):
|
||||
"""Handle manager finished event."""
|
||||
print("Foreign toplevel manager finished")
|
||||
self.running = False
|
||||
|
||||
def get_windows(self) -> List[Window]:
|
||||
"""Get all currently active windows."""
|
||||
# Filter out closed windows
|
||||
active_windows = [
|
||||
window for window in self.windows.values() if not window.closed
|
||||
]
|
||||
return active_windows
|
||||
|
||||
def run(self):
|
||||
"""Run the event loop to receive window updates."""
|
||||
self.running = True
|
||||
print("Listening for window events (press Ctrl+C to exit)...")
|
||||
|
||||
try:
|
||||
while self.running:
|
||||
self.display.dispatch(block=True)
|
||||
except KeyboardInterrupt:
|
||||
print("\nExiting...")
|
||||
finally:
|
||||
self.cleanup()
|
||||
|
||||
def cleanup(self):
|
||||
"""Clean up resources."""
|
||||
print("cleanup")
|
||||
if self.manager:
|
||||
self.manager.stop()
|
||||
|
||||
# Destroy all toplevel handles
|
||||
for toplevel, window in list(self.windows.items()):
|
||||
if not window.closed:
|
||||
toplevel.destroy()
|
||||
|
||||
# Disconnect from display
|
||||
if self.display:
|
||||
self.display.disconnect()
|
||||
|
||||
self.running = False
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
manager = WaylandWindowManager()
|
||||
|
||||
if not manager.connect():
|
||||
return 1
|
||||
|
||||
# # Run for a short time to collect initial windows
|
||||
for _ in range(1):
|
||||
manager.display.dispatch(block=True)
|
||||
|
||||
# Print all windows
|
||||
windows = manager.get_windows()
|
||||
print("\nActive windows:")
|
||||
if windows:
|
||||
for i, window in enumerate(windows, 1):
|
||||
print(f"{i}. {window}")
|
||||
else:
|
||||
print("No windows found")
|
||||
|
||||
# # Option to keep monitoring window events
|
||||
# if len(sys.argv) > 1 and sys.argv[1] == "--monitor":
|
||||
# manager.run()
|
||||
# else:
|
||||
manager.cleanup()
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,270 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<protocol name="wlr_foreign_toplevel_management_unstable_v1">
|
||||
<copyright>
|
||||
Copyright © 2018 Ilia Bozhinov
|
||||
|
||||
Permission to use, copy, modify, distribute, and sell this
|
||||
software and its documentation for any purpose is hereby granted
|
||||
without fee, provided that the above copyright notice appear in
|
||||
all copies and that both that copyright notice and this permission
|
||||
notice appear in supporting documentation, and that the name of
|
||||
the copyright holders not be used in advertising or publicity
|
||||
pertaining to distribution of the software without specific,
|
||||
written prior permission. The copyright holders make no
|
||||
representations about the suitability of this software for any
|
||||
purpose. It is provided "as is" without express or implied
|
||||
warranty.
|
||||
|
||||
THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
|
||||
SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
|
||||
AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
|
||||
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
||||
THIS SOFTWARE.
|
||||
</copyright>
|
||||
|
||||
<interface name="zwlr_foreign_toplevel_manager_v1" version="3">
|
||||
<description summary="list and control opened apps">
|
||||
The purpose of this protocol is to enable the creation of taskbars
|
||||
and docks by providing them with a list of opened applications and
|
||||
letting them request certain actions on them, like maximizing, etc.
|
||||
|
||||
After a client binds the zwlr_foreign_toplevel_manager_v1, each opened
|
||||
toplevel window will be sent via the toplevel event
|
||||
</description>
|
||||
|
||||
<event name="toplevel">
|
||||
<description summary="a toplevel has been created">
|
||||
This event is emitted whenever a new toplevel window is created. It
|
||||
is emitted for all toplevels, regardless of the app that has created
|
||||
them.
|
||||
|
||||
All initial details of the toplevel(title, app_id, states, etc.) will
|
||||
be sent immediately after this event via the corresponding events in
|
||||
zwlr_foreign_toplevel_handle_v1.
|
||||
</description>
|
||||
<arg name="toplevel" type="new_id" interface="zwlr_foreign_toplevel_handle_v1"/>
|
||||
</event>
|
||||
|
||||
<request name="stop">
|
||||
<description summary="stop sending events">
|
||||
Indicates the client no longer wishes to receive events for new toplevels.
|
||||
However the compositor may emit further toplevel_created events, until
|
||||
the finished event is emitted.
|
||||
|
||||
The client must not send any more requests after this one.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<event name="finished" type="destructor">
|
||||
<description summary="the compositor has finished with the toplevel manager">
|
||||
This event indicates that the compositor is done sending events to the
|
||||
zwlr_foreign_toplevel_manager_v1. The server will destroy the object
|
||||
immediately after sending this request, so it will become invalid and
|
||||
the client should free any resources associated with it.
|
||||
</description>
|
||||
</event>
|
||||
</interface>
|
||||
|
||||
<interface name="zwlr_foreign_toplevel_handle_v1" version="3">
|
||||
<description summary="an opened toplevel">
|
||||
A zwlr_foreign_toplevel_handle_v1 object represents an opened toplevel
|
||||
window. Each app may have multiple opened toplevels.
|
||||
|
||||
Each toplevel has a list of outputs it is visible on, conveyed to the
|
||||
client with the output_enter and output_leave events.
|
||||
</description>
|
||||
|
||||
<event name="title">
|
||||
<description summary="title change">
|
||||
This event is emitted whenever the title of the toplevel changes.
|
||||
</description>
|
||||
<arg name="title" type="string"/>
|
||||
</event>
|
||||
|
||||
<event name="app_id">
|
||||
<description summary="app-id change">
|
||||
This event is emitted whenever the app-id of the toplevel changes.
|
||||
</description>
|
||||
<arg name="app_id" type="string"/>
|
||||
</event>
|
||||
|
||||
<event name="output_enter">
|
||||
<description summary="toplevel entered an output">
|
||||
This event is emitted whenever the toplevel becomes visible on
|
||||
the given output. A toplevel may be visible on multiple outputs.
|
||||
</description>
|
||||
<arg name="output" type="object" interface="wl_output"/>
|
||||
</event>
|
||||
|
||||
<event name="output_leave">
|
||||
<description summary="toplevel left an output">
|
||||
This event is emitted whenever the toplevel stops being visible on
|
||||
the given output. It is guaranteed that an entered-output event
|
||||
with the same output has been emitted before this event.
|
||||
</description>
|
||||
<arg name="output" type="object" interface="wl_output"/>
|
||||
</event>
|
||||
|
||||
<request name="set_maximized">
|
||||
<description summary="requests that the toplevel be maximized">
|
||||
Requests that the toplevel be maximized. If the maximized state actually
|
||||
changes, this will be indicated by the state event.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<request name="unset_maximized">
|
||||
<description summary="requests that the toplevel be unmaximized">
|
||||
Requests that the toplevel be unmaximized. If the maximized state actually
|
||||
changes, this will be indicated by the state event.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<request name="set_minimized">
|
||||
<description summary="requests that the toplevel be minimized">
|
||||
Requests that the toplevel be minimized. If the minimized state actually
|
||||
changes, this will be indicated by the state event.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<request name="unset_minimized">
|
||||
<description summary="requests that the toplevel be unminimized">
|
||||
Requests that the toplevel be unminimized. If the minimized state actually
|
||||
changes, this will be indicated by the state event.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<request name="activate">
|
||||
<description summary="activate the toplevel">
|
||||
Request that this toplevel be activated on the given seat.
|
||||
There is no guarantee the toplevel will be actually activated.
|
||||
</description>
|
||||
<arg name="seat" type="object" interface="wl_seat"/>
|
||||
</request>
|
||||
|
||||
<enum name="state">
|
||||
<description summary="types of states on the toplevel">
|
||||
The different states that a toplevel can have. These have the same meaning
|
||||
as the states with the same names defined in xdg-toplevel
|
||||
</description>
|
||||
|
||||
<entry name="maximized" value="0" summary="the toplevel is maximized"/>
|
||||
<entry name="minimized" value="1" summary="the toplevel is minimized"/>
|
||||
<entry name="activated" value="2" summary="the toplevel is active"/>
|
||||
<entry name="fullscreen" value="3" summary="the toplevel is fullscreen" since="2"/>
|
||||
</enum>
|
||||
|
||||
<event name="state">
|
||||
<description summary="the toplevel state changed">
|
||||
This event is emitted immediately after the zlw_foreign_toplevel_handle_v1
|
||||
is created and each time the toplevel state changes, either because of a
|
||||
compositor action or because of a request in this protocol.
|
||||
</description>
|
||||
|
||||
<arg name="state" type="array"/>
|
||||
</event>
|
||||
|
||||
<event name="done">
|
||||
<description summary="all information about the toplevel has been sent">
|
||||
This event is sent after all changes in the toplevel state have been
|
||||
sent.
|
||||
|
||||
This allows changes to the zwlr_foreign_toplevel_handle_v1 properties
|
||||
to be seen as atomic, even if they happen via multiple events.
|
||||
</description>
|
||||
</event>
|
||||
|
||||
<request name="close">
|
||||
<description summary="request that the toplevel be closed">
|
||||
Send a request to the toplevel to close itself. The compositor would
|
||||
typically use a shell-specific method to carry out this request, for
|
||||
example by sending the xdg_toplevel.close event. However, this gives
|
||||
no guarantees the toplevel will actually be destroyed. If and when
|
||||
this happens, the zwlr_foreign_toplevel_handle_v1.closed event will
|
||||
be emitted.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<request name="set_rectangle">
|
||||
<description summary="the rectangle which represents the toplevel">
|
||||
The rectangle of the surface specified in this request corresponds to
|
||||
the place where the app using this protocol represents the given toplevel.
|
||||
It can be used by the compositor as a hint for some operations, e.g
|
||||
minimizing. The client is however not required to set this, in which
|
||||
case the compositor is free to decide some default value.
|
||||
|
||||
If the client specifies more than one rectangle, only the last one is
|
||||
considered.
|
||||
|
||||
The dimensions are given in surface-local coordinates.
|
||||
Setting width=height=0 removes the already-set rectangle.
|
||||
</description>
|
||||
|
||||
<arg name="surface" type="object" interface="wl_surface"/>
|
||||
<arg name="x" type="int"/>
|
||||
<arg name="y" type="int"/>
|
||||
<arg name="width" type="int"/>
|
||||
<arg name="height" type="int"/>
|
||||
</request>
|
||||
|
||||
<enum name="error">
|
||||
<entry name="invalid_rectangle" value="0"
|
||||
summary="the provided rectangle is invalid"/>
|
||||
</enum>
|
||||
|
||||
<event name="closed">
|
||||
<description summary="this toplevel has been destroyed">
|
||||
This event means the toplevel has been destroyed. It is guaranteed there
|
||||
won't be any more events for this zwlr_foreign_toplevel_handle_v1. The
|
||||
toplevel itself becomes inert so any requests will be ignored except the
|
||||
destroy request.
|
||||
</description>
|
||||
</event>
|
||||
|
||||
<request name="destroy" type="destructor">
|
||||
<description summary="destroy the zwlr_foreign_toplevel_handle_v1 object">
|
||||
Destroys the zwlr_foreign_toplevel_handle_v1 object.
|
||||
|
||||
This request should be called either when the client does not want to
|
||||
use the toplevel anymore or after the closed event to finalize the
|
||||
destruction of the object.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<!-- Version 2 additions -->
|
||||
|
||||
<request name="set_fullscreen" since="2">
|
||||
<description summary="request that the toplevel be fullscreened">
|
||||
Requests that the toplevel be fullscreened on the given output. If the
|
||||
fullscreen state and/or the outputs the toplevel is visible on actually
|
||||
change, this will be indicated by the state and output_enter/leave
|
||||
events.
|
||||
|
||||
The output parameter is only a hint to the compositor. Also, if output
|
||||
is NULL, the compositor should decide which output the toplevel will be
|
||||
fullscreened on, if at all.
|
||||
</description>
|
||||
<arg name="output" type="object" interface="wl_output" allow-null="true"/>
|
||||
</request>
|
||||
|
||||
<request name="unset_fullscreen" since="2">
|
||||
<description summary="request that the toplevel be unfullscreened">
|
||||
Requests that the toplevel be unfullscreened. If the fullscreen state
|
||||
actually changes, this will be indicated by the state event.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<!-- Version 3 additions -->
|
||||
|
||||
<event name="parent" since="3">
|
||||
<description summary="parent change">
|
||||
This event is emitted whenever the parent of the toplevel changes.
|
||||
|
||||
No event is emitted when the parent handle is destroyed by the client.
|
||||
</description>
|
||||
<arg name="parent" type="object" interface="zwlr_foreign_toplevel_handle_v1" allow-null="true"/>
|
||||
</event>
|
||||
</interface>
|
||||
</protocol>
|
||||
@@ -1,27 +0,0 @@
|
||||
# This file has been autogenerated by the pywayland scanner
|
||||
|
||||
# Copyright © 2018 Ilia Bozhinov
|
||||
#
|
||||
# Permission to use, copy, modify, distribute, and sell this
|
||||
# software and its documentation for any purpose is hereby granted
|
||||
# without fee, provided that the above copyright notice appear in
|
||||
# all copies and that both that copyright notice and this permission
|
||||
# notice appear in supporting documentation, and that the name of
|
||||
# the copyright holders not be used in advertising or publicity
|
||||
# pertaining to distribution of the software without specific,
|
||||
# written prior permission. The copyright holders make no
|
||||
# representations about the suitability of this software for any
|
||||
# purpose. It is provided "as is" without express or implied
|
||||
# warranty.
|
||||
#
|
||||
# THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
|
||||
# SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
# FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
# SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
|
||||
# AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
|
||||
# ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
||||
# THIS SOFTWARE.
|
||||
|
||||
from .zwlr_foreign_toplevel_handle_v1 import ZwlrForeignToplevelHandleV1 # noqa: F401
|
||||
from .zwlr_foreign_toplevel_manager_v1 import ZwlrForeignToplevelManagerV1 # noqa: F401
|
||||
@@ -1,352 +0,0 @@
|
||||
# This file has been autogenerated by the pywayland scanner
|
||||
|
||||
# Copyright © 2018 Ilia Bozhinov
|
||||
#
|
||||
# Permission to use, copy, modify, distribute, and sell this
|
||||
# software and its documentation for any purpose is hereby granted
|
||||
# without fee, provided that the above copyright notice appear in
|
||||
# all copies and that both that copyright notice and this permission
|
||||
# notice appear in supporting documentation, and that the name of
|
||||
# the copyright holders not be used in advertising or publicity
|
||||
# pertaining to distribution of the software without specific,
|
||||
# written prior permission. The copyright holders make no
|
||||
# representations about the suitability of this software for any
|
||||
# purpose. It is provided "as is" without express or implied
|
||||
# warranty.
|
||||
#
|
||||
# THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
|
||||
# SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
# FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
# SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
|
||||
# AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
|
||||
# ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
||||
# THIS SOFTWARE.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
|
||||
from pywayland.protocol_core import (
|
||||
Argument,
|
||||
ArgumentType,
|
||||
Global,
|
||||
Interface,
|
||||
Proxy,
|
||||
Resource,
|
||||
)
|
||||
|
||||
from pywayland.protocol.wayland import WlOutput
|
||||
from pywayland.protocol.wayland import WlSeat
|
||||
from pywayland.protocol.wayland import WlSurface
|
||||
|
||||
|
||||
class ZwlrForeignToplevelHandleV1(Interface):
|
||||
"""An opened toplevel
|
||||
|
||||
A :class:`ZwlrForeignToplevelHandleV1` object represents an opened toplevel
|
||||
window. Each app may have multiple opened toplevels.
|
||||
|
||||
Each toplevel has a list of outputs it is visible on, conveyed to the
|
||||
client with the output_enter and output_leave events.
|
||||
"""
|
||||
|
||||
name = "zwlr_foreign_toplevel_handle_v1"
|
||||
version = 3
|
||||
|
||||
class state(enum.IntEnum):
|
||||
maximized = 0
|
||||
minimized = 1
|
||||
activated = 2
|
||||
fullscreen = 3
|
||||
|
||||
class error(enum.IntEnum):
|
||||
invalid_rectangle = 0
|
||||
|
||||
|
||||
class ZwlrForeignToplevelHandleV1Proxy(Proxy[ZwlrForeignToplevelHandleV1]):
|
||||
interface = ZwlrForeignToplevelHandleV1
|
||||
|
||||
@ZwlrForeignToplevelHandleV1.request()
|
||||
def set_maximized(self) -> None:
|
||||
"""Requests that the toplevel be maximized
|
||||
|
||||
Requests that the toplevel be maximized. If the maximized state
|
||||
actually changes, this will be indicated by the state event.
|
||||
"""
|
||||
self._marshal(0)
|
||||
|
||||
@ZwlrForeignToplevelHandleV1.request()
|
||||
def unset_maximized(self) -> None:
|
||||
"""Requests that the toplevel be unmaximized
|
||||
|
||||
Requests that the toplevel be unmaximized. If the maximized state
|
||||
actually changes, this will be indicated by the state event.
|
||||
"""
|
||||
self._marshal(1)
|
||||
|
||||
@ZwlrForeignToplevelHandleV1.request()
|
||||
def set_minimized(self) -> None:
|
||||
"""Requests that the toplevel be minimized
|
||||
|
||||
Requests that the toplevel be minimized. If the minimized state
|
||||
actually changes, this will be indicated by the state event.
|
||||
"""
|
||||
self._marshal(2)
|
||||
|
||||
@ZwlrForeignToplevelHandleV1.request()
|
||||
def unset_minimized(self) -> None:
|
||||
"""Requests that the toplevel be unminimized
|
||||
|
||||
Requests that the toplevel be unminimized. If the minimized state
|
||||
actually changes, this will be indicated by the state event.
|
||||
"""
|
||||
self._marshal(3)
|
||||
|
||||
@ZwlrForeignToplevelHandleV1.request(
|
||||
Argument(ArgumentType.Object, interface=WlSeat),
|
||||
)
|
||||
def activate(self, seat: WlSeat) -> None:
|
||||
"""Activate the toplevel
|
||||
|
||||
Request that this toplevel be activated on the given seat. There is no
|
||||
guarantee the toplevel will be actually activated.
|
||||
|
||||
:param seat:
|
||||
:type seat:
|
||||
:class:`~pywayland.protocol.wayland.WlSeat`
|
||||
"""
|
||||
self._marshal(4, seat)
|
||||
|
||||
@ZwlrForeignToplevelHandleV1.request()
|
||||
def close(self) -> None:
|
||||
"""Request that the toplevel be closed
|
||||
|
||||
Send a request to the toplevel to close itself. The compositor would
|
||||
typically use a shell-specific method to carry out this request, for
|
||||
example by sending the xdg_toplevel.close event. However, this gives no
|
||||
guarantees the toplevel will actually be destroyed. If and when this
|
||||
happens, the :func:`ZwlrForeignToplevelHandleV1.closed()` event will be
|
||||
emitted.
|
||||
"""
|
||||
self._marshal(5)
|
||||
|
||||
@ZwlrForeignToplevelHandleV1.request(
|
||||
Argument(ArgumentType.Object, interface=WlSurface),
|
||||
Argument(ArgumentType.Int),
|
||||
Argument(ArgumentType.Int),
|
||||
Argument(ArgumentType.Int),
|
||||
Argument(ArgumentType.Int),
|
||||
)
|
||||
def set_rectangle(
|
||||
self, surface: WlSurface, x: int, y: int, width: int, height: int
|
||||
) -> None:
|
||||
"""The rectangle which represents the toplevel
|
||||
|
||||
The rectangle of the surface specified in this request corresponds to
|
||||
the place where the app using this protocol represents the given
|
||||
toplevel. It can be used by the compositor as a hint for some
|
||||
operations, e.g minimizing. The client is however not required to set
|
||||
this, in which case the compositor is free to decide some default
|
||||
value.
|
||||
|
||||
If the client specifies more than one rectangle, only the last one is
|
||||
considered.
|
||||
|
||||
The dimensions are given in surface-local coordinates. Setting
|
||||
width=height=0 removes the already-set rectangle.
|
||||
|
||||
:param surface:
|
||||
:type surface:
|
||||
:class:`~pywayland.protocol.wayland.WlSurface`
|
||||
:param x:
|
||||
:type x:
|
||||
`ArgumentType.Int`
|
||||
:param y:
|
||||
:type y:
|
||||
`ArgumentType.Int`
|
||||
:param width:
|
||||
:type width:
|
||||
`ArgumentType.Int`
|
||||
:param height:
|
||||
:type height:
|
||||
`ArgumentType.Int`
|
||||
"""
|
||||
self._marshal(6, surface, x, y, width, height)
|
||||
|
||||
@ZwlrForeignToplevelHandleV1.request()
|
||||
def destroy(self) -> None:
|
||||
"""Destroy the :class:`ZwlrForeignToplevelHandleV1` object
|
||||
|
||||
Destroys the :class:`ZwlrForeignToplevelHandleV1` object.
|
||||
|
||||
This request should be called either when the client does not want to
|
||||
use the toplevel anymore or after the closed event to finalize the
|
||||
destruction of the object.
|
||||
"""
|
||||
self._marshal(7)
|
||||
self._destroy()
|
||||
|
||||
@ZwlrForeignToplevelHandleV1.request(
|
||||
Argument(ArgumentType.Object, interface=WlOutput, nullable=True),
|
||||
version=2,
|
||||
)
|
||||
def set_fullscreen(self, output: WlOutput | None) -> None:
|
||||
"""Request that the toplevel be fullscreened
|
||||
|
||||
Requests that the toplevel be fullscreened on the given output. If the
|
||||
fullscreen state and/or the outputs the toplevel is visible on actually
|
||||
change, this will be indicated by the state and output_enter/leave
|
||||
events.
|
||||
|
||||
The output parameter is only a hint to the compositor. Also, if output
|
||||
is NULL, the compositor should decide which output the toplevel will be
|
||||
fullscreened on, if at all.
|
||||
|
||||
:param output:
|
||||
:type output:
|
||||
:class:`~pywayland.protocol.wayland.WlOutput` or `None`
|
||||
"""
|
||||
self._marshal(8, output)
|
||||
|
||||
@ZwlrForeignToplevelHandleV1.request(version=2)
|
||||
def unset_fullscreen(self) -> None:
|
||||
"""Request that the toplevel be unfullscreened
|
||||
|
||||
Requests that the toplevel be unfullscreened. If the fullscreen state
|
||||
actually changes, this will be indicated by the state event.
|
||||
"""
|
||||
self._marshal(9)
|
||||
|
||||
|
||||
class ZwlrForeignToplevelHandleV1Resource(Resource):
|
||||
interface = ZwlrForeignToplevelHandleV1
|
||||
|
||||
@ZwlrForeignToplevelHandleV1.event(
|
||||
Argument(ArgumentType.String),
|
||||
)
|
||||
def title(self, title: str) -> None:
|
||||
"""Title change
|
||||
|
||||
This event is emitted whenever the title of the toplevel changes.
|
||||
|
||||
:param title:
|
||||
:type title:
|
||||
`ArgumentType.String`
|
||||
"""
|
||||
self._post_event(0, title)
|
||||
|
||||
@ZwlrForeignToplevelHandleV1.event(
|
||||
Argument(ArgumentType.String),
|
||||
)
|
||||
def app_id(self, app_id: str) -> None:
|
||||
"""App-id change
|
||||
|
||||
This event is emitted whenever the app-id of the toplevel changes.
|
||||
|
||||
:param app_id:
|
||||
:type app_id:
|
||||
`ArgumentType.String`
|
||||
"""
|
||||
self._post_event(1, app_id)
|
||||
|
||||
@ZwlrForeignToplevelHandleV1.event(
|
||||
Argument(ArgumentType.Object, interface=WlOutput),
|
||||
)
|
||||
def output_enter(self, output: WlOutput) -> None:
|
||||
"""Toplevel entered an output
|
||||
|
||||
This event is emitted whenever the toplevel becomes visible on the
|
||||
given output. A toplevel may be visible on multiple outputs.
|
||||
|
||||
:param output:
|
||||
:type output:
|
||||
:class:`~pywayland.protocol.wayland.WlOutput`
|
||||
"""
|
||||
self._post_event(2, output)
|
||||
|
||||
@ZwlrForeignToplevelHandleV1.event(
|
||||
Argument(ArgumentType.Object, interface=WlOutput),
|
||||
)
|
||||
def output_leave(self, output: WlOutput) -> None:
|
||||
"""Toplevel left an output
|
||||
|
||||
This event is emitted whenever the toplevel stops being visible on the
|
||||
given output. It is guaranteed that an entered-output event with the
|
||||
same output has been emitted before this event.
|
||||
|
||||
:param output:
|
||||
:type output:
|
||||
:class:`~pywayland.protocol.wayland.WlOutput`
|
||||
"""
|
||||
self._post_event(3, output)
|
||||
|
||||
@ZwlrForeignToplevelHandleV1.event(
|
||||
Argument(ArgumentType.Array),
|
||||
)
|
||||
def state(self, state: list) -> None:
|
||||
"""The toplevel state changed
|
||||
|
||||
This event is emitted immediately after the
|
||||
zlw_foreign_toplevel_handle_v1 is created and each time the toplevel
|
||||
state changes, either because of a compositor action or because of a
|
||||
request in this protocol.
|
||||
|
||||
:param state:
|
||||
:type state:
|
||||
`ArgumentType.Array`
|
||||
"""
|
||||
self._post_event(4, state)
|
||||
|
||||
@ZwlrForeignToplevelHandleV1.event()
|
||||
def done(self) -> None:
|
||||
"""All information about the toplevel has been sent
|
||||
|
||||
This event is sent after all changes in the toplevel state have been
|
||||
sent.
|
||||
|
||||
This allows changes to the :class:`ZwlrForeignToplevelHandleV1`
|
||||
properties to be seen as atomic, even if they happen via multiple
|
||||
events.
|
||||
"""
|
||||
self._post_event(5)
|
||||
|
||||
@ZwlrForeignToplevelHandleV1.event()
|
||||
def closed(self) -> None:
|
||||
"""This toplevel has been destroyed
|
||||
|
||||
This event means the toplevel has been destroyed. It is guaranteed
|
||||
there won't be any more events for this
|
||||
:class:`ZwlrForeignToplevelHandleV1`. The toplevel itself becomes inert
|
||||
so any requests will be ignored except the destroy request.
|
||||
"""
|
||||
self._post_event(6)
|
||||
|
||||
@ZwlrForeignToplevelHandleV1.event(
|
||||
Argument(
|
||||
ArgumentType.Object, interface=ZwlrForeignToplevelHandleV1, nullable=True
|
||||
),
|
||||
version=3,
|
||||
)
|
||||
def parent(self, parent: ZwlrForeignToplevelHandleV1 | None) -> None:
|
||||
"""Parent change
|
||||
|
||||
This event is emitted whenever the parent of the toplevel changes.
|
||||
|
||||
No event is emitted when the parent handle is destroyed by the client.
|
||||
|
||||
:param parent:
|
||||
:type parent:
|
||||
:class:`ZwlrForeignToplevelHandleV1` or `None`
|
||||
"""
|
||||
self._post_event(7, parent)
|
||||
|
||||
|
||||
class ZwlrForeignToplevelHandleV1Global(Global):
|
||||
interface = ZwlrForeignToplevelHandleV1
|
||||
|
||||
|
||||
ZwlrForeignToplevelHandleV1._gen_c()
|
||||
ZwlrForeignToplevelHandleV1.proxy_class = ZwlrForeignToplevelHandleV1Proxy
|
||||
ZwlrForeignToplevelHandleV1.resource_class = ZwlrForeignToplevelHandleV1Resource
|
||||
ZwlrForeignToplevelHandleV1.global_class = ZwlrForeignToplevelHandleV1Global
|
||||
@@ -1,112 +0,0 @@
|
||||
# This file has been autogenerated by the pywayland scanner
|
||||
|
||||
# Copyright © 2018 Ilia Bozhinov
|
||||
#
|
||||
# Permission to use, copy, modify, distribute, and sell this
|
||||
# software and its documentation for any purpose is hereby granted
|
||||
# without fee, provided that the above copyright notice appear in
|
||||
# all copies and that both that copyright notice and this permission
|
||||
# notice appear in supporting documentation, and that the name of
|
||||
# the copyright holders not be used in advertising or publicity
|
||||
# pertaining to distribution of the software without specific,
|
||||
# written prior permission. The copyright holders make no
|
||||
# representations about the suitability of this software for any
|
||||
# purpose. It is provided "as is" without express or implied
|
||||
# warranty.
|
||||
#
|
||||
# THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
|
||||
# SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
# FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
# SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
|
||||
# AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
|
||||
# ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
||||
# THIS SOFTWARE.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pywayland.protocol_core import (
|
||||
Argument,
|
||||
ArgumentType,
|
||||
Global,
|
||||
Interface,
|
||||
Proxy,
|
||||
Resource,
|
||||
)
|
||||
|
||||
from .zwlr_foreign_toplevel_handle_v1 import ZwlrForeignToplevelHandleV1
|
||||
|
||||
|
||||
class ZwlrForeignToplevelManagerV1(Interface):
|
||||
"""List and control opened apps
|
||||
|
||||
The purpose of this protocol is to enable the creation of taskbars and
|
||||
docks by providing them with a list of opened applications and letting them
|
||||
request certain actions on them, like maximizing, etc.
|
||||
|
||||
After a client binds the :class:`ZwlrForeignToplevelManagerV1`, each opened
|
||||
toplevel window will be sent via the toplevel event
|
||||
"""
|
||||
|
||||
name = "zwlr_foreign_toplevel_manager_v1"
|
||||
version = 3
|
||||
|
||||
|
||||
class ZwlrForeignToplevelManagerV1Proxy(Proxy[ZwlrForeignToplevelManagerV1]):
|
||||
interface = ZwlrForeignToplevelManagerV1
|
||||
|
||||
@ZwlrForeignToplevelManagerV1.request()
|
||||
def stop(self) -> None:
|
||||
"""Stop sending events
|
||||
|
||||
Indicates the client no longer wishes to receive events for new
|
||||
toplevels. However the compositor may emit further toplevel_created
|
||||
events, until the finished event is emitted.
|
||||
|
||||
The client must not send any more requests after this one.
|
||||
"""
|
||||
self._marshal(0)
|
||||
|
||||
|
||||
class ZwlrForeignToplevelManagerV1Resource(Resource):
|
||||
interface = ZwlrForeignToplevelManagerV1
|
||||
|
||||
@ZwlrForeignToplevelManagerV1.event(
|
||||
Argument(ArgumentType.NewId, interface=ZwlrForeignToplevelHandleV1),
|
||||
)
|
||||
def toplevel(self, toplevel: ZwlrForeignToplevelHandleV1) -> None:
|
||||
"""A toplevel has been created
|
||||
|
||||
This event is emitted whenever a new toplevel window is created. It is
|
||||
emitted for all toplevels, regardless of the app that has created them.
|
||||
|
||||
All initial details of the toplevel(title, app_id, states, etc.) will
|
||||
be sent immediately after this event via the corresponding events in
|
||||
:class:`~pywayland.protocol.wlr_foreign_toplevel_management_unstable_v1.ZwlrForeignToplevelHandleV1`.
|
||||
|
||||
:param toplevel:
|
||||
:type toplevel:
|
||||
:class:`~pywayland.protocol.wlr_foreign_toplevel_management_unstable_v1.ZwlrForeignToplevelHandleV1`
|
||||
"""
|
||||
self._post_event(0, toplevel)
|
||||
|
||||
@ZwlrForeignToplevelManagerV1.event()
|
||||
def finished(self) -> None:
|
||||
"""The compositor has finished with the toplevel manager
|
||||
|
||||
This event indicates that the compositor is done sending events to the
|
||||
:class:`ZwlrForeignToplevelManagerV1`. The server will destroy the
|
||||
object immediately after sending this request, so it will become
|
||||
invalid and the client should free any resources associated with it.
|
||||
"""
|
||||
self._post_event(1)
|
||||
|
||||
|
||||
class ZwlrForeignToplevelManagerV1Global(Global):
|
||||
interface = ZwlrForeignToplevelManagerV1
|
||||
|
||||
|
||||
ZwlrForeignToplevelManagerV1._gen_c()
|
||||
ZwlrForeignToplevelManagerV1.proxy_class = ZwlrForeignToplevelManagerV1Proxy
|
||||
ZwlrForeignToplevelManagerV1.resource_class = ZwlrForeignToplevelManagerV1Resource
|
||||
ZwlrForeignToplevelManagerV1.global_class = ZwlrForeignToplevelManagerV1Global
|
||||
@@ -1,238 +0,0 @@
|
||||
import time
|
||||
from gi.repository import GLib
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from pywayland.client import Display
|
||||
from pywayland.protocol.wayland import WlOutput, WlSeat
|
||||
|
||||
from fabric.core.service import Property, Service, Signal
|
||||
from fabric.utils.helpers import idle_add
|
||||
|
||||
from bar.services.wlr.protocol.wlr_foreign_toplevel_management_unstable_v1.zwlr_foreign_toplevel_manager_v1 import (
|
||||
ZwlrForeignToplevelManagerV1,
|
||||
)
|
||||
from bar.services.wlr.protocol.wlr_foreign_toplevel_management_unstable_v1.zwlr_foreign_toplevel_handle_v1 import (
|
||||
ZwlrForeignToplevelHandleV1,
|
||||
)
|
||||
|
||||
|
||||
class Window:
|
||||
"""Represents a toplevel window in the compositor."""
|
||||
|
||||
def __init__(self, handle: ZwlrForeignToplevelHandleV1):
|
||||
self.handle = handle
|
||||
self.title: str = "Unknown"
|
||||
self.app_id: str = "Unknown"
|
||||
self.states: List[str] = []
|
||||
self.outputs: List[WlOutput] = []
|
||||
self.parent: Optional["Window"] = None
|
||||
self.closed = False
|
||||
|
||||
def __str__(self) -> str:
|
||||
state_str = (
|
||||
", ".join([ZwlrForeignToplevelHandleV1.state(s).name for s in self.states])
|
||||
if self.states
|
||||
else "normal"
|
||||
)
|
||||
return (
|
||||
f"Window(title='{self.title}', app_id='{self.app_id}', state={state_str})"
|
||||
)
|
||||
|
||||
|
||||
class WaylandWindowTracker(Service):
|
||||
"""Track Wayland windows in the background and provide access on demand."""
|
||||
|
||||
@Property(bool, "readable", "is-ready", default_value=False)
|
||||
def ready(self) -> bool:
|
||||
return self._ready
|
||||
|
||||
@Signal
|
||||
def ready_signal(self):
|
||||
return self.notify("ready")
|
||||
|
||||
@Property(list[Window], "readable", "windows")
|
||||
def windows(self) -> list[Window]:
|
||||
return [window for window in self._window_dict.values() if not window.closed]
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.display = None
|
||||
self._window_dict: Dict[ZwlrForeignToplevelHandleV1, Window] = {}
|
||||
self._windows = []
|
||||
self.manager = None
|
||||
self.seat: Optional[WlSeat] = None
|
||||
|
||||
self.thread = GLib.Thread.new(
|
||||
"wayland-window-service", self._run_display_thread
|
||||
)
|
||||
|
||||
def _run_display_thread(self):
|
||||
"""Run the Wayland event loop in a background thread."""
|
||||
try:
|
||||
self.display = Display()
|
||||
self.display.connect()
|
||||
|
||||
# Get the registry to find the foreign toplevel manager
|
||||
registry = self.display.get_registry()
|
||||
registry.dispatcher["global"] = self._registry_global_handler
|
||||
|
||||
# Process registry events
|
||||
self.display.roundtrip()
|
||||
|
||||
if not self.manager:
|
||||
print("Foreign toplevel manager not found")
|
||||
return
|
||||
|
||||
# Process more events to get initial windows
|
||||
for _ in range(5):
|
||||
self.display.roundtrip()
|
||||
|
||||
idle_add(self._set_ready)
|
||||
|
||||
while True:
|
||||
self.display.dispatch(block=True)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Display thread error: {e}")
|
||||
finally:
|
||||
self.cleanup()
|
||||
|
||||
def _registry_global_handler(self, registry, id, interface, version):
|
||||
"""Handle registry global objects."""
|
||||
if interface == WlSeat.name:
|
||||
self.seat = registry.bind(id, WlSeat, version)
|
||||
print(f"Found seat (id={id}, version={version})")
|
||||
elif interface == ZwlrForeignToplevelManagerV1.name:
|
||||
self.manager = registry.bind(
|
||||
id, ZwlrForeignToplevelManagerV1, min(version, 3)
|
||||
)
|
||||
self.manager.dispatcher["toplevel"] = self._handle_toplevel
|
||||
self.manager.dispatcher["finished"] = self._handle_manager_finished
|
||||
|
||||
def _handle_toplevel(self, manager, toplevel):
|
||||
"""Handle a new toplevel window."""
|
||||
print("TOPLEVEL IS TRIGGERD")
|
||||
window = Window(toplevel)
|
||||
|
||||
self._window_dict[toplevel] = window
|
||||
|
||||
# Setup event dispatchers for the toplevel
|
||||
toplevel.dispatcher["title"] = self._handle_title
|
||||
toplevel.dispatcher["app_id"] = self._handle_app_id
|
||||
toplevel.dispatcher["state"] = self._handle_state
|
||||
toplevel.dispatcher["done"] = self._handle_done
|
||||
toplevel.dispatcher["closed"] = self._handle_closed
|
||||
toplevel.dispatcher["output_enter"] = self._handle_output_enter
|
||||
toplevel.dispatcher["output_leave"] = self._handle_output_leave
|
||||
|
||||
def _handle_title(self, toplevel, title):
|
||||
"""Handle toplevel title changes."""
|
||||
window = self._window_dict.get(toplevel)
|
||||
if window:
|
||||
print("there is a window, putting title")
|
||||
window.title = title
|
||||
|
||||
def _handle_app_id(self, toplevel, app_id):
|
||||
"""Handle toplevel app_id changes."""
|
||||
window = self._window_dict.get(toplevel)
|
||||
if window:
|
||||
window.app_id = app_id
|
||||
|
||||
def _handle_state(self, toplevel, states):
|
||||
"""Handle toplevel state changes."""
|
||||
window = self._window_dict.get(toplevel)
|
||||
if window:
|
||||
window.states = states
|
||||
|
||||
def _handle_done(self, toplevel):
|
||||
"""Handle toplevel done event."""
|
||||
# We don't need to print anything here as we're just tracking silently
|
||||
pass
|
||||
|
||||
def _handle_closed(self, toplevel):
|
||||
"""Handle toplevel closed event."""
|
||||
window = self._window_dict.get(toplevel)
|
||||
if window:
|
||||
window.closed = True
|
||||
# Remove from our dictionary
|
||||
del self._window_dict[toplevel]
|
||||
|
||||
# Clean up the toplevel object
|
||||
toplevel.destroy()
|
||||
|
||||
def _handle_output_enter(self, toplevel, output):
|
||||
"""Handle toplevel entering an output."""
|
||||
window = self._window_dict.get(toplevel)
|
||||
if window and output not in window.outputs:
|
||||
window.outputs.append(output)
|
||||
|
||||
def _handle_output_leave(self, toplevel, output):
|
||||
"""Handle toplevel leaving an output."""
|
||||
window = self._window_dict.get(toplevel)
|
||||
if window and output in window.outputs:
|
||||
window.outputs.remove(output)
|
||||
|
||||
def _handle_parent(self, toplevel, parent):
|
||||
"""Handle toplevel parent changes."""
|
||||
window = self._window_dict.get(toplevel)
|
||||
if window:
|
||||
if parent is None:
|
||||
window.parent = None
|
||||
else:
|
||||
parent_window = self._window_dict.get(parent)
|
||||
if parent_window:
|
||||
window.parent = parent_window
|
||||
|
||||
def _handle_manager_finished(self, manager):
|
||||
"""Handle manager finished event."""
|
||||
self.running = False
|
||||
|
||||
def _set_ready(self):
|
||||
print("IM READY")
|
||||
self._ready = True
|
||||
self.ready_signal.emit()
|
||||
return False
|
||||
|
||||
def get_windows(self) -> List[Window]:
|
||||
"""Get all currently active windows."""
|
||||
print([window for window in self._window_dict.values()])
|
||||
print("YOU CALLED WINDOWS")
|
||||
return [window for window in self._window_dict.values() if not window.closed]
|
||||
|
||||
def activate_window(self, window: Window):
|
||||
if self.seat is None:
|
||||
print("Cannot activate window: no seat available")
|
||||
return
|
||||
|
||||
print(f"Activating window: {window.title}")
|
||||
window.handle.activate(self.seat)
|
||||
self.display.flush() # flush the request to the Wayland server
|
||||
|
||||
def cleanup(self):
|
||||
"""Clean up resources."""
|
||||
self.running = False
|
||||
print("Cleanup")
|
||||
|
||||
if self.manager:
|
||||
try:
|
||||
self.manager.stop()
|
||||
except:
|
||||
pass
|
||||
|
||||
# Disconnect from display
|
||||
if self.display:
|
||||
try:
|
||||
self.display.disconnect()
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def print_windows(tracker):
|
||||
"""Print the current list of windows."""
|
||||
windows = tracker.get_windows()
|
||||
print(f"\nCurrent windows ({len(windows)}):")
|
||||
if windows:
|
||||
for i, window in enumerate(windows, 1):
|
||||
print(f"{i}. {window}")
|
||||
else:
|
||||
print("No windows found")
|
||||
@@ -3,7 +3,6 @@
|
||||
border-bottom: solid 2px;
|
||||
border-color: var(--border-color);
|
||||
background-color: var(--window-bg);
|
||||
min-height: 28px;
|
||||
}
|
||||
|
||||
#center-container {
|
||||
@@ -15,6 +14,27 @@
|
||||
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,
|
||||
#ram-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);
|
||||
--module-bg: alpha(var(--mid-bg), 0.8);
|
||||
--border-color: var(--light-bg);
|
||||
--ws-active: var(--pink);
|
||||
--ws-inactive: var(--blue);
|
||||
--ws-active: var(--blue);
|
||||
--ws-visible: var(--violet);
|
||||
--ws-inactive: var(--light-grey);
|
||||
--ws-empty: var(--dark-grey);
|
||||
--ws-hover: var(--turquoise);
|
||||
--ws-urgent: var(--red);
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
@import url("./vinyl.css");
|
||||
@import url("./bar.css");
|
||||
@import url("./finder.css");
|
||||
@import url("./calendar.css");
|
||||
@import url("./notmuch.css");
|
||||
|
||||
|
||||
/* 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 {
|
||||
padding: 0px 8px;
|
||||
transition: padding 0.05s steps(8);
|
||||
background-color: rgba(180, 180, 180, 0.2);
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s ease;
|
||||
background-color: var(--module-bg);
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
#vinyl-button:hover {
|
||||
background-color: var(--light-bg);
|
||||
}
|
||||
|
||||
/* Active state styling */
|
||||
.active #vinyl-button {
|
||||
background-color: rgba(108, 158, 175, 0.7);
|
||||
padding: 0px 32px;
|
||||
#vinyl-button.active {
|
||||
background-color: var(--pink);
|
||||
}
|
||||
|
||||
#vinyl-button.active:hover {
|
||||
background-color: var(--turquoise);
|
||||
}
|
||||
|
||||
/* Icon styling */
|
||||
#vinyl-icon {
|
||||
color: #555555;
|
||||
min-width: 36px;
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
/* Label styling */
|
||||
#vinyl-label {
|
||||
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);
|
||||
#vinyl-button.active #vinyl-icon {
|
||||
color: var(--background);
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
|
||||
#workspaces>button {
|
||||
padding: 0px 8px;
|
||||
transition: padding 0.05s steps(8);
|
||||
background-color: var(--foreground);
|
||||
transition: padding 0.05s steps(8), background-color 0.15s ease;
|
||||
background-color: var(--ws-inactive);
|
||||
border-radius: 100px;
|
||||
}
|
||||
|
||||
@@ -15,11 +15,24 @@
|
||||
font-size: 0px;
|
||||
}
|
||||
|
||||
#workspaces button.hover {
|
||||
#workspaces>button: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);
|
||||
color: var(--foreground);
|
||||
font-weight: bold;
|
||||
@@ -31,12 +44,3 @@
|
||||
50% { opacity: 0.5; }
|
||||
100% { opacity: 1.0; }
|
||||
}
|
||||
|
||||
#workspaces>button.empty {
|
||||
background-color: var(--ws-empty);
|
||||
}
|
||||
|
||||
#workspaces>button.active {
|
||||
padding: 0px 32px;
|
||||
background-color: var(--ws-active);
|
||||
}
|
||||
|
||||
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:
|
||||
enabled: true
|
||||
enable: true
|
||||
battery:
|
||||
enable: true
|
||||
|
||||
6
flake.lock
generated
6
flake.lock
generated
@@ -6,11 +6,11 @@
|
||||
"utils": "utils"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1745289078,
|
||||
"narHash": "sha256-1dZTqsWPaHyWjZkX4MaJdwUAQoMXwr8hhHymxQIwFrY=",
|
||||
"lastModified": 1770146720,
|
||||
"narHash": "sha256-YVlwsUz4SLj8qYAb21ernT3lDB/piU1V6hTW/UjikWA=",
|
||||
"owner": "Fabric-Development",
|
||||
"repo": "fabric",
|
||||
"rev": "1831ced4d9bb9f4be3893be55a8d502b47bff29e",
|
||||
"rev": "fd2aabbd7e1859aa7c11c626a6c36a937aca736a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
89
flake.nix
89
flake.nix
@@ -35,23 +35,21 @@
|
||||
packages = {
|
||||
default = pkgs.callPackage ./nix/derivation.nix { inherit (pkgs) lib python3Packages; };
|
||||
makku = pkgs.writeShellScriptBin "makku" ''
|
||||
dbus-send --session --print-reply --dest=org.Fabric.fabric.bar /org/Fabric/fabric org.Fabric.fabric.Evaluate string:"finder.open()" > /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";
|
||||
program = "${self.packages.${system}.default}/bin/bar";
|
||||
};
|
||||
show = {
|
||||
type = "app";
|
||||
program = "${self.packages.${system}.makku}/bin/makku";
|
||||
};
|
||||
};
|
||||
}
|
||||
)
|
||||
// {
|
||||
homeManagerModules.makku-bar =
|
||||
homeManagerModules = {
|
||||
makku-bar =
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
@@ -82,10 +80,81 @@
|
||||
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 = {
|
||||
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";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -112,5 +181,7 @@
|
||||
};
|
||||
};
|
||||
};
|
||||
stylix-makku-bar = import ./nix/stylix/hm.nix;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
wrapGAppsHook3,
|
||||
playerctl,
|
||||
webp-pixbuf-loader,
|
||||
notmuch,
|
||||
khal,
|
||||
emacs,
|
||||
...
|
||||
}:
|
||||
|
||||
@@ -38,6 +41,8 @@ python3Packages.buildPythonApplication {
|
||||
gdk-pixbuf
|
||||
playerctl
|
||||
webp-pixbuf-loader
|
||||
notmuch
|
||||
khal
|
||||
];
|
||||
|
||||
dependencies = with python3Packages; [
|
||||
@@ -60,13 +65,19 @@ python3Packages.buildPythonApplication {
|
||||
cp scripts/launcher.py $out/bin/bar
|
||||
chmod +x $out/bin/bar
|
||||
|
||||
|
||||
runHook postInstall
|
||||
'';
|
||||
|
||||
preFixup = ''
|
||||
makeWrapperArgs+=("''${gappsWrapperArgs[@]}")
|
||||
makeWrapperArgs+=(--prefix PATH : ${lib.makeBinPath [ khal notmuch emacs ]})
|
||||
'';
|
||||
|
||||
passthru = {
|
||||
inherit khal notmuch emacs;
|
||||
};
|
||||
|
||||
meta = {
|
||||
changelog = "";
|
||||
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