feat(trayapp): add system tray app to monitor keyd layer state
This commit is contained in:
166
src/keyd-layer-tray/__main__.py
Normal file
166
src/keyd-layer-tray/__main__.py
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
import threading
|
||||||
|
import signal
|
||||||
|
from PyQt5.QtWidgets import QApplication, QSystemTrayIcon
|
||||||
|
from PyQt5.QtGui import QIcon, QPixmap
|
||||||
|
from PyQt5.QtCore import QObject, pyqtSignal, QTimer
|
||||||
|
|
||||||
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
LAYER_NAME = "sup" # Target layer name to monitor
|
||||||
|
|
||||||
|
def create_icon(symbol: str, color: str) -> QIcon:
|
||||||
|
"""
|
||||||
|
Create a QIcon containing a single centered symbol.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
symbol (str): The symbol/character to draw (e.g., ∑, ÷, ×).
|
||||||
|
color (str): The color to render the symbol in (e.g., "green", "gray").
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
QIcon: A QIcon object ready to be used in a system tray.
|
||||||
|
"""
|
||||||
|
size = 64
|
||||||
|
img = Image.new("RGBA", (size, size), (0, 0, 0, 0))
|
||||||
|
d = ImageDraw.Draw(img)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Load bold DejaVu font for better visibility
|
||||||
|
font = ImageFont.truetype("DejaVuSans-Bold.ttf", 60)
|
||||||
|
except IOError:
|
||||||
|
# Fallback if font is not available
|
||||||
|
font = ImageFont.load_default()
|
||||||
|
|
||||||
|
# Get bounding box of the symbol
|
||||||
|
bbox = d.textbbox((0, 0), symbol, font=font)
|
||||||
|
w, h = bbox[2] - bbox[0], bbox[3] - bbox[1]
|
||||||
|
|
||||||
|
# Compute centered coordinates
|
||||||
|
x = (size - w) / 2 - bbox[0]
|
||||||
|
y = (size - h) / 2 - bbox[1]
|
||||||
|
|
||||||
|
# Draw the symbol
|
||||||
|
d.text((x, y), symbol, font=font, fill=color)
|
||||||
|
|
||||||
|
# Convert Pillow image -> PNG in memory -> QPixmap -> QIcon
|
||||||
|
buf = BytesIO()
|
||||||
|
img.save(buf, format="PNG")
|
||||||
|
qpixmap = QPixmap()
|
||||||
|
qpixmap.loadFromData(buf.getvalue(), "PNG")
|
||||||
|
return QIcon(qpixmap)
|
||||||
|
|
||||||
|
|
||||||
|
class KeydListener(QObject):
|
||||||
|
"""
|
||||||
|
Background listener for keyd events.
|
||||||
|
|
||||||
|
Runs `keyd listen` in a subprocess and emits `state_changed` signals
|
||||||
|
whenever the target layer ("+sup" or "-sup") is activated/deactivated.
|
||||||
|
"""
|
||||||
|
state_changed = pyqtSignal(bool)
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
# Start listener in a background thread
|
||||||
|
self.thread = threading.Thread(target=self.run, daemon=True) # type: ignore
|
||||||
|
self.thread.start() # type: ignore
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""
|
||||||
|
Continuously read lines from `keyd listen` and emit events based on state.
|
||||||
|
"""
|
||||||
|
proc = subprocess.Popen(["keyd", "listen"], stdout=subprocess.PIPE, text=True)
|
||||||
|
for line in proc.stdout: # type: ignore
|
||||||
|
line = line.strip()
|
||||||
|
if line == f"+{LAYER_NAME}":
|
||||||
|
self.state_changed.emit(True)
|
||||||
|
elif line == f"-{LAYER_NAME}":
|
||||||
|
self.state_changed.emit(False)
|
||||||
|
|
||||||
|
|
||||||
|
# Global flag used to gracefully exit the application
|
||||||
|
exit_requested = False
|
||||||
|
|
||||||
|
|
||||||
|
def handle_exit_signal(signum, frame):
|
||||||
|
"""
|
||||||
|
Signal handler for SIGINT and SIGTERM.
|
||||||
|
Sets global exit_requested flag to True so that the main loop can stop.
|
||||||
|
"""
|
||||||
|
global exit_requested
|
||||||
|
exit_requested = True
|
||||||
|
|
||||||
|
|
||||||
|
class TrayApp:
|
||||||
|
"""
|
||||||
|
System tray application that displays the state of the keyd "sup" layer.
|
||||||
|
|
||||||
|
Shows a green icon when active, a gray icon when inactive.
|
||||||
|
Reacts to signals from KeydListener and updates the tray icon accordingly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# Initialize Qt application
|
||||||
|
self.app = QApplication(sys.argv)
|
||||||
|
|
||||||
|
# Prepare icons for active/inactive state
|
||||||
|
self.icon_on = create_icon("∑", "green")
|
||||||
|
self.icon_off = create_icon("∑", "gray")
|
||||||
|
|
||||||
|
# Create system tray icon
|
||||||
|
self.tray = QSystemTrayIcon(self.icon_off)
|
||||||
|
self.tray.setToolTip("Math layer: OFF")
|
||||||
|
self.tray.show()
|
||||||
|
|
||||||
|
# Start keyd listener
|
||||||
|
self.listener = KeydListener()
|
||||||
|
self.listener.state_changed.connect(self.update_icon)
|
||||||
|
|
||||||
|
# Register signal handlers for Ctrl+C / SIGTERM
|
||||||
|
signal.signal(signal.SIGINT, handle_exit_signal)
|
||||||
|
signal.signal(signal.SIGTERM, handle_exit_signal)
|
||||||
|
|
||||||
|
# Use QTimer to periodically check for exit flag
|
||||||
|
self.timer = QTimer()
|
||||||
|
self.timer.timeout.connect(self.check_exit)
|
||||||
|
self.timer.start(200)
|
||||||
|
|
||||||
|
def update_icon(self, active: bool):
|
||||||
|
"""
|
||||||
|
Update tray icon and tooltip depending on active state.
|
||||||
|
"""
|
||||||
|
if active:
|
||||||
|
self.tray.setIcon(self.icon_on)
|
||||||
|
self.tray.setToolTip("Math layer: ON")
|
||||||
|
else:
|
||||||
|
self.tray.setIcon(self.icon_off)
|
||||||
|
self.tray.setToolTip("Math layer: OFF")
|
||||||
|
|
||||||
|
def check_exit(self):
|
||||||
|
"""
|
||||||
|
Periodically check if exit has been requested via signal handler.
|
||||||
|
"""
|
||||||
|
global exit_requested
|
||||||
|
if exit_requested:
|
||||||
|
print("Exiting Math-Layer-Tray …")
|
||||||
|
self.tray.hide()
|
||||||
|
self.app.quit()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""
|
||||||
|
Start the Qt application main loop.
|
||||||
|
"""
|
||||||
|
sys.exit(self.app.exec_())
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""
|
||||||
|
Entry point for the tray application.
|
||||||
|
"""
|
||||||
|
TrayApp().run()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user