"""Colours, fonts and the application stylesheet. One dark theme, tuned by hand: near-black charcoal surfaces a step apart, a single warm accent (the bolt's amber), and colour used only where it carries meaning — green for done, red for failed, amber for "needs you". """ from __future__ import annotations import sys from PySide6.QtGui import QColor, QFont, QFontDatabase, QPalette BG = "#0e1014" # window SURFACE = "#15181e" # cards, composer SURFACE_HI = "#1b1f27" # hover / user bubble BORDER = "#252a34" BORDER_HI = "#343b48" TEXT = "#e7e9ee" TEXT_DIM = "#9aa1ae" TEXT_FAINT = "#6b7280" ACCENT = "#f5b83d" # the bolt ACCENT_HI = "#ffc95c" ACCENT_INK = "#1a1305" # text on accent GREEN = "#4ec98a" RED = "#f06a6a" BLUE = "#7aa7ff" CODE = "#e8c98f" # inline code in replies DIFF_ADD_BG = "#12291d" DIFF_DEL_BG = "#301719" RADIUS = 12 COLUMN_WIDTH = 820 # the transcript's reading column def _first_available(families, fallback: str) -> str: installed = set(QFontDatabase.families()) for family in families: if family in installed: return family return fallback def ui_font_family() -> str: return _first_available( ["Segoe UI Variable Text", "Inter", "SF Pro Text", "Segoe UI", "Cantarell", "Ubuntu"], QFont().defaultFamily(), ) def mono_font_family() -> str: return _first_available( ["Cascadia Code", "Cascadia Mono", "JetBrains Mono", "SF Mono", "Consolas", "DejaVu Sans Mono", "Menlo"], "monospace", ) def mono_font(size: float = 9.5) -> QFont: font = QFont(mono_font_family()) font.setPointSizeF(size) font.setStyleHint(QFont.Monospace) return font def palette() -> QPalette: pal = QPalette() for role, colour in ( (QPalette.Window, BG), (QPalette.Base, SURFACE), (QPalette.AlternateBase, SURFACE_HI), (QPalette.Text, TEXT), (QPalette.WindowText, TEXT), (QPalette.ButtonText, TEXT), (QPalette.Button, SURFACE), (QPalette.Highlight, "#3a3222"), (QPalette.HighlightedText, TEXT), (QPalette.Link, BLUE), (QPalette.PlaceholderText, TEXT_FAINT), (QPalette.ToolTipBase, SURFACE_HI), (QPalette.ToolTipText, TEXT), ): pal.setColor(role, QColor(colour)) return pal def stylesheet() -> str: mono = mono_font_family() return f""" QWidget {{ color: {TEXT}; font-size: 10.5pt; }} QMainWindow, #Root {{ background: {BG}; }} QToolTip {{ background: {SURFACE_HI}; color: {TEXT}; border: 1px solid {BORDER_HI}; border-radius: 6px; padding: 5px 8px; }} /* header */ #Header {{ background: {BG}; border-bottom: 1px solid {BORDER}; }} #Wordmark {{ font-size: 12pt; font-weight: 600; letter-spacing: 0.3px; }} #WordmarkSub {{ color: {TEXT_FAINT}; font-size: 9pt; }} #Chip {{ background: {SURFACE}; border: 1px solid {BORDER}; border-radius: 15px; padding: 5px 12px 5px 30px; color: {TEXT_DIM}; text-align: left; }} #Chip:hover {{ border-color: {BORDER_HI}; color: {TEXT}; background: {SURFACE_HI}; }} #Chip:disabled {{ color: {TEXT_FAINT}; }} #Segment {{ background: {SURFACE}; border: 1px solid {BORDER}; border-radius: 15px; }} #SegmentButton {{ background: transparent; border: none; border-radius: 12px; padding: 4px 14px; color: {TEXT_DIM}; font-weight: 500; }} #SegmentButton:hover {{ color: {TEXT}; }} #SegmentButton:checked {{ background: {SURFACE_HI}; color: {TEXT}; }} #SegmentButton[danger="true"]:checked {{ background: #3a2c12; color: {ACCENT_HI}; }} /* transcript */ QScrollArea {{ background: transparent; border: none; }} #Transcript {{ background: {BG}; }} QScrollBar:vertical {{ background: transparent; width: 10px; margin: 4px 2px; }} QScrollBar::handle:vertical {{ background: {BORDER}; border-radius: 3px; min-height: 40px; }} QScrollBar::handle:vertical:hover {{ background: {BORDER_HI}; }} QScrollBar::add-line, QScrollBar::sub-line, QScrollBar::add-page, QScrollBar::sub-page {{ background: none; height: 0; }} QScrollBar:horizontal {{ height: 0; }} #UserBubble {{ background: {SURFACE_HI}; border: 1px solid {BORDER}; border-radius: 16px; }} #AssistantText {{ color: {TEXT}; }} #Holding {{ color: {TEXT_FAINT}; font-style: italic; }} #Notice {{ color: {TEXT_DIM}; font-size: 9.5pt; }} #NoticeError {{ color: {RED}; }} #ToolCard {{ background: {SURFACE}; border: 1px solid {BORDER}; border-radius: {RADIUS}px; }} #ToolCard[state="waiting"] {{ border-color: #5a4418; }} #ToolHeader {{ background: transparent; border: none; border-radius: {RADIUS}px; }} #ToolHeader:hover {{ background: {SURFACE_HI}; }} #ToolName {{ font-weight: 600; }} #ToolArg {{ color: {TEXT_DIM}; font-family: "{mono}"; font-size: 9.5pt; }} #ToolOutput {{ background: transparent; color: {TEXT_DIM}; border: none; border-top: 1px solid {BORDER}; font-family: "{mono}"; font-size: 9.2pt; padding: 10px 14px; }} #Pill {{ border-radius: 9px; padding: 1px 9px; font-size: 8.5pt; font-weight: 600; }} #Pill[kind="running"] {{ background: #1c2433; color: {BLUE}; }} #Pill[kind="ok"] {{ background: #13271d; color: {GREEN}; }} #Pill[kind="error"] {{ background: #2e1618; color: {RED}; }} #Pill[kind="waiting"] {{ background: #33280f; color: {ACCENT}; }} #Pill[kind="declined"] {{ background: #22252c; color: {TEXT_DIM}; }} #ApprovalBar {{ border-top: 1px solid {BORDER}; background: transparent; }} #ApprovalQuestion {{ color: {TEXT}; }} #Primary {{ background: {ACCENT}; color: {ACCENT_INK}; border: none; border-radius: 8px; padding: 6px 16px; font-weight: 600; }} #Primary:hover {{ background: {ACCENT_HI}; }} #Secondary {{ background: {SURFACE_HI}; color: {TEXT}; border: 1px solid {BORDER_HI}; border-radius: 8px; padding: 6px 14px; }} #Secondary:hover {{ border-color: {TEXT_FAINT}; }} #Ghost {{ background: transparent; color: {TEXT_DIM}; border: none; padding: 6px 10px; }} #Ghost:hover {{ color: {TEXT}; }} /* empty state */ #Hero {{ font-size: 19pt; font-weight: 600; }} #HeroSub {{ color: {TEXT_DIM}; }} #Suggestion {{ background: {SURFACE}; border: 1px solid {BORDER}; border-radius: 12px; padding: 12px 14px; color: {TEXT_DIM}; text-align: left; }} #Suggestion:hover {{ border-color: {BORDER_HI}; color: {TEXT}; background: {SURFACE_HI}; }} /* composer */ #Composer {{ background: {SURFACE}; border: 1px solid {BORDER}; border-radius: 18px; }} #Composer[focused="true"] {{ border-color: {BORDER_HI}; }} #ComposerEdit {{ background: transparent; border: none; padding: 0; selection-background-color: #3a3222; }} #Hint {{ color: {TEXT_FAINT}; font-size: 8.8pt; }} /* chats dialog */ QDialog#ChatDialog {{ background: {BG}; }} #ChatList {{ background: {SURFACE}; border: 1px solid {BORDER}; border-radius: {RADIUS}px; padding: 4px; outline: none; }} #ChatList::item {{ border-radius: 9px; margin: 1px 0; }} #ChatList::item:hover {{ background: {SURFACE_HI}; }} #ChatList::item:selected {{ background: #262017; border: 1px solid #4a3a18; }} #ChatTitle {{ font-weight: 600; }} #ChatTime {{ color: {TEXT_FAINT}; font-size: 9pt; }} #ChatPreview {{ color: {TEXT_DIM}; font-size: 9.5pt; }} """ def dark_title_bar(window) -> None: """Windows 11: dark caption, painted the same colour as the window so the title bar and the app read as one surface. Silently does nothing on other platforms or older Windows.""" if sys.platform != "win32": return try: import ctypes from ctypes import wintypes hwnd = wintypes.HWND(int(window.winId())) dwm = ctypes.windll.dwmapi def attr(key: int, value: int) -> None: data = ctypes.c_int(value) dwm.DwmSetWindowAttribute(hwnd, key, ctypes.byref(data), ctypes.sizeof(data)) attr(20, 1) # DWMWA_USE_IMMERSIVE_DARK_MODE colour = BG.lstrip("#") r, g, b = int(colour[0:2], 16), int(colour[2:4], 16), int(colour[4:6], 16) attr(35, (b << 16) | (g << 8) | r) # DWMWA_CAPTION_COLOR (COLORREF is 0x00BBGGRR) border = BORDER.lstrip("#") r, g, b = int(border[0:2], 16), int(border[2:4], 16), int(border[4:6], 16) attr(34, (b << 16) | (g << 8) | r) # DWMWA_BORDER_COLOR except Exception: pass