|
#!/usr/bin/env python3 |
|
|
|
import json |
|
import os |
|
import pty |
|
import re |
|
import select |
|
import subprocess |
|
from datetime import datetime |
|
|
|
# ANSI color code to Pango color mapping |
|
ANSI_COLORS = { |
|
"30": "#000000", "31": "#ff5555", "32": "#00ff00", "33": "#ffff00", |
|
"34": "#5599ff", "35": "#ff00ff", "36": "#66d9ef", "37": "#ffffff", |
|
"90": "#555555", "91": "#ff5555", "92": "#55ff55", "93": "#ffff55", |
|
"94": "#79b8ff", "95": "#ff55ff", "96": "#88e0ef", "97": "#ffffff", |
|
} |
|
|
|
|
|
def run_khal_with_pty(): |
|
"""Run khal in a PTY so it outputs ANSI colors.""" |
|
master, slave = pty.openpty() |
|
try: |
|
p = subprocess.Popen( |
|
["khal", "calendar"], |
|
stdout=slave, stderr=subprocess.DEVNULL, close_fds=True |
|
) |
|
os.close(slave) |
|
output = b"" |
|
while True: |
|
r, _, _ = select.select([master], [], [], 3) |
|
if not r: |
|
break |
|
try: |
|
data = os.read(master, 4096) |
|
if not data: |
|
break |
|
output += data |
|
except OSError: |
|
break |
|
p.wait() |
|
finally: |
|
try: |
|
os.close(master) |
|
except OSError: |
|
pass |
|
return output.decode("utf-8", errors="replace").replace("\r\n", "\n").strip() |
|
|
|
|
|
def ansi_to_pango(text): |
|
"""Convert ANSI escape sequences to Pango markup.""" |
|
# Escape Pango special chars first (but not things inside ANSI sequences) |
|
# Split on ANSI sequences, escape the text parts, then reassemble |
|
parts = re.split(r"(\x1b\[[0-9;]*m)", text) |
|
|
|
result = [] |
|
open_spans = 0 |
|
|
|
for part in parts: |
|
m = re.match(r"\x1b\[([0-9;]*)m", part) |
|
if m: |
|
codes = m.group(1).split(";") if m.group(1) else ["0"] |
|
|
|
# Reset |
|
if codes == ["0"] or codes == [""]: |
|
result.append("</span>" * open_spans) |
|
open_spans = 0 |
|
continue |
|
|
|
attrs = [] |
|
bold = False |
|
color = None |
|
reverse = False |
|
|
|
for code in codes: |
|
if code == "1": |
|
bold = True |
|
elif code == "7": |
|
reverse = True |
|
elif code in ANSI_COLORS: |
|
color = ANSI_COLORS[code] |
|
|
|
if reverse: |
|
# Reverse video: use white bg, dark fg for "today" highlight |
|
attrs.append("background='#ffffff'") |
|
attrs.append("foreground='#1e1e2e'") |
|
elif color: |
|
attrs.append(f"foreground='{color}'") |
|
if bold: |
|
attrs.append("weight='bold'") |
|
|
|
if attrs: |
|
result.append(f"<span {' '.join(attrs)}>") |
|
open_spans += 1 |
|
else: |
|
# Escape Pango markup characters in text |
|
part = part.replace("&", "&").replace("<", "<").replace(">", ">") |
|
result.append(part) |
|
|
|
# Close any remaining open spans |
|
result.append("</span>" * open_spans) |
|
return "".join(result) |
|
|
|
|
|
now = datetime.now() |
|
clock_text = now.strftime("%Y/%m/%d %H:%M") |
|
|
|
try: |
|
raw_output = run_khal_with_pty() |
|
khal_output = ansi_to_pango(raw_output) |
|
except Exception: |
|
khal_output = "khal unavailable" |
|
|
|
print(json.dumps({ |
|
"text": clock_text, |
|
"tooltip": f"<tt><span font_family='JetBrainsMono'>{khal_output}</span></tt>", |
|
"markup": "pango", |
|
"class": "clock" |
|
})) |