Newer
Older
navi-1 / clients / terminal / tui / themes.py
"""Color themes for the Navi Code TUI.

The default dark theme uses the exact gnexus-ui-kit cyberpunk palette
(light Tokyo Night influence) found in webclient/vendor/gnexus-ui-kit.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import ClassVar

from textual.color import Color
from textual.theme import Theme as TextualTheme


@dataclass(frozen=True)
class Theme:
    """A complete color theme for the TUI."""

    name: str
    background: Color
    surface: Color
    panel: Color
    border: Color
    primary: Color
    primary_dark: Color
    secondary: Color
    accent: Color
    text: Color
    text_muted: Color
    text_dim: Color
    success: Color
    warning: Color
    error: Color
    info: Color
    user_bubble: Color
    assistant_bubble: Color
    thinking: Color
    tool_border: Color
    tool_success: Color
    tool_error: Color
    status_online: Color
    status_offline: Color
    prompt_border: Color
    selection: Color
    link: Color

    def css(self) -> dict[str, str]:
        """Return CSS variable dictionary for Textual."""
        return {
            "tui-background": self.background.hex,
            "tui-surface": self.surface.hex,
            "tui-panel": self.panel.hex,
            "tui-border": self.border.hex,
            "tui-primary": self.primary.hex,
            "tui-primary-dark": self.primary_dark.hex,
            "tui-secondary": self.secondary.hex,
            "tui-accent": self.accent.hex,
            "tui-text": self.text.hex,
            "tui-text-muted": self.text_muted.hex,
            "tui-text-dim": self.text_dim.hex,
            "tui-success": self.success.hex,
            "tui-warning": self.warning.hex,
            "tui-error": self.error.hex,
            "tui-info": self.info.hex,
            "tui-user-bubble": self.user_bubble.hex,
            "tui-assistant-bubble": self.assistant_bubble.hex,
            "tui-thinking": self.thinking.hex,
            "tui-tool-border": self.tool_border.hex,
            "tui-tool-success": self.tool_success.hex,
            "tui-tool-error": self.tool_error.hex,
            "tui-status-online": self.status_online.hex,
            "tui-status-offline": self.status_offline.hex,
            "tui-prompt-border": self.prompt_border.hex,
            "tui-selection": self.selection.hex,
            "tui-link": self.link.hex,
        }

    def to_textual_theme(self) -> TextualTheme:
        """Return a registered Textual theme that installs our CSS variables."""
        return TextualTheme(
            name=self.name,
            primary=self.primary.hex,
            secondary=self.secondary.hex,
            warning=self.warning.hex,
            error=self.error.hex,
            success=self.success.hex,
            accent=self.accent.hex,
            foreground=self.text.hex,
            background=self.background.hex,
            surface=self.surface.hex,
            panel=self.panel.hex,
            variables=self.css(),
        )

    def to_css_string(self) -> str:
        """Generate a Textual CSS string with all theme variables."""
        lines = ["/* Auto-generated TUI theme */"]
        for key, value in self.css().items():
            lines.append(f"$ {key}: {value};")
        return "\n".join(lines)


# ── gnexus-ui-kit palette (webclient/vendor/gnexus-ui-kit/src/scss/_palette-colors.scss) ─
# neutrals
#   $color-black:        #16161E
#   $color-dark:         #1F2335
#   $color-grey:         #414868
# prime neons
#   $color-cyan:         #7DCFFF
#   $color-magenta:      #FF00CC
#   $color-hot-pink:     #FF1492
#   $color-electric-blue:#7AA2F7
#   $color-orange:       #FF9E64
# secondary accents
#   $color-purple:       #BB9AF7
#   $color-indigo:       #565F89
#   $color-teal:         #73DACA
# highlights
#   $color-neon-yellow:  #E0AF68
#   $color-neon-green:   #9ECE6A
# text tones
#   $color-text-light:   #C0CAF5
#   $color-text-medium:  #A9B1D6
#   $color-text-dark:    #787C99
# UI state
#   $color-primary:      $color-text-light
#   $color-secondary:    $color-electric-blue
#   $color-accent:       $color-orange
#   $color-success:      $color-neon-green
#   $color-warning:      $color-neon-yellow
#   $color-error:        #F7768E
#   $color-info:         $color-purple

GNEXUS_DARK = Theme(
    name="gnexus-dark",
    background=Color.parse("#16161E"),
    surface=Color.parse("#1F2335"),
    panel=Color.parse("#24283B"),  # slightly lighter than surface for panels
    border=Color.parse("#414868"),
    primary=Color.parse("#C0CAF5"),  # text-light, primary UI color
    primary_dark=Color.parse("#A9B1D6"),
    secondary=Color.parse("#7AA2F7"),  # electric-blue
    accent=Color.parse("#FF9E64"),  # orange
    text=Color.parse("#C0CAF5"),
    text_muted=Color.parse("#A9B1D6"),
    text_dim=Color.parse("#787C99"),
    success=Color.parse("#9ECE6A"),  # neon-green
    warning=Color.parse("#E0AF68"),  # neon-yellow
    error=Color.parse("#F7768E"),
    info=Color.parse("#BB9AF7"),  # purple
    user_bubble=Color.parse("#565F89"),  # indigo
    assistant_bubble=Color.parse("#1F2335"),
    thinking=Color.parse("#787C99"),
    tool_border=Color.parse("#FF9E64"),
    tool_success=Color.parse("#9ECE6A"),
    tool_error=Color.parse("#F7768E"),
    status_online=Color.parse("#9ECE6A"),
    status_offline=Color.parse("#F7768E"),
    prompt_border=Color.parse("#7DCFFF"),  # cyan highlight for input
    selection=Color.parse("#FF00CC"),  # magenta — active selection / cursor line
    link=Color.parse("#73DACA"),  # teal — URLs and references
)


GNEXUS_LIGHT = Theme(
    name="gnexus-light",
    background=Color.parse("#F5F6FA"),
    surface=Color.parse("#FFFFFF"),
    panel=Color.parse("#ECEEF5"),
    border=Color.parse("#A9B1D6"),
    primary=Color.parse("#1F2335"),
    primary_dark=Color.parse("#16161E"),
    secondary=Color.parse("#2B3A67"),
    accent=Color.parse("#FF6C00"),
    text=Color.parse("#16161E"),
    text_muted=Color.parse("#414868"),
    text_dim=Color.parse("#787C99"),
    success=Color.parse("#3F8F2A"),
    warning=Color.parse("#B45309"),
    error=Color.parse("#C41E3A"),
    info=Color.parse("#7C3AED"),
    user_bubble=Color.parse("#DBEAFE"),
    assistant_bubble=Color.parse("#FFFFFF"),
    thinking=Color.parse("#787C99"),
    tool_border=Color.parse("#B45309"),
    tool_success=Color.parse("#3F8F2A"),
    tool_error=Color.parse("#C41E3A"),
    status_online=Color.parse("#3F8F2A"),
    status_offline=Color.parse("#C41E3A"),
    prompt_border=Color.parse("#7AA2F7"),
    selection=Color.parse("#FF1492"),  # hot-pink — active selection / cursor line
    link=Color.parse("#0D9488"),  # darker teal for readability on light background
)


_active_theme_name: str = "gnexus-dark"


def set_active_theme(name: str) -> None:
    """Mark the named theme as the currently active one."""
    global _active_theme_name
    _active_theme_name = name


def get_active_theme() -> Theme:
    """Return the currently active theme, falling back to the dark default."""
    return ThemeRegistry.get(_active_theme_name)


class ThemeRegistry:
    """Store and resolve themes by name."""

    _themes: ClassVar[dict[str, Theme]] = {}

    @classmethod
    def register(cls, theme: Theme) -> None:
        cls._themes[theme.name] = theme

    @classmethod
    def get(cls, name: str) -> Theme:
        if name not in cls._themes:
            return cls._themes.get("gnexus-dark", GNEXUS_DARK)
        return cls._themes[name]

    @classmethod
    def all(cls) -> list[str]:
        return list(cls._themes.keys())


ThemeRegistry.register(GNEXUS_DARK)
ThemeRegistry.register(GNEXUS_LIGHT)