vibefpmgui.themes

View → Theme: the colors of the whole window (the editor's syntax colors are View → Syntax Colors; with those on Automatic, a theme picks the matching Pygments style).

System, Light and Dark keep the platform's own look (on macOS: its buttons, tabs and dialogs) and only choose light or dark, through Qt's color scheme. The colorful themes need a style that draws with the palette's colors (macOS's doesn't), so they use Qt's Fusion style with a palette of their own; the color scheme is still set to light or dark to match, so the window's title bar and native dialogs agree.

View → Window Style chooses the style itself instead (any of Qt's on this platform: Fusion everywhere, macOS on a Mac, windows11 / windowsvista on Windows…). Fusion draws the panels' dividers with a grip of dots, macOS's a plain line; only Fusion (and the plain Windows style) show a colorful theme's colors throughout.

Either style is wrapped in StillStyle, which turns off the moving of panels: Qt's dock layout could otherwise keep "animating" forever on macOS.

  1"""*View → Theme*: the colors of the whole window (the editor's syntax colors
  2are *View → Syntax Colors*; with those on Automatic, a theme picks the
  3matching Pygments style).
  4
  5*System*, *Light* and *Dark* keep the platform's own look (on macOS: its
  6buttons, tabs and dialogs) and only choose light or dark, through Qt's color
  7scheme. The colorful themes need a style that draws with the palette's
  8colors (macOS's doesn't), so they use Qt's Fusion style with a palette of
  9their own; the color scheme is still set to light or dark to match, so the
 10window's title bar and native dialogs agree.
 11
 12*View → Window Style* chooses the style itself instead (any of Qt's on this
 13platform: Fusion everywhere, macOS on a Mac, windows11 / windowsvista on
 14Windows…). Fusion draws the panels' dividers with a grip of dots, macOS's a
 15plain line; only Fusion (and the plain Windows style) show a colorful
 16theme's colors throughout.
 17
 18Either style is wrapped in `StillStyle`, which turns off the moving of
 19panels: Qt's dock layout could otherwise keep "animating" forever on macOS.
 20"""
 21
 22from typing import NamedTuple
 23
 24from qtpy.QtCore import Qt
 25from qtpy.QtGui import QColor, QFont, QPalette
 26from qtpy.QtWidgets import QApplication, QProxyStyle, QStyle, QStyleFactory, QTabBar
 27
 28
 29class Theme(NamedTuple):
 30    title: str
 31    dark: bool
 32    window: str = ""        # "" for the platform's own (System, Light, Dark)
 33    base: str = ""          # text fields, lists, the editor's surroundings
 34    text: str = ""
 35    button: str = ""
 36    highlight: str = ""     # selection and accents
 37    highlighted_text: str = ""
 38    link: str = ""
 39    syntax: str = ""        # the Pygments style for Syntax Colors → Automatic ("": light/dark default)
 40
 41    @property
 42    def native(self) -> bool:
 43        """The platform's look (only light or dark chosen), not our palette."""
 44        return not self.window
 45
 46
 47SYSTEM = "system"
 48THEMES: dict[str, Theme] = {
 49    SYSTEM: Theme("System (follow light/dark mode)", False),
 50    "light": Theme("Light", False),
 51    "dark": Theme("Dark", True),
 52    # Dark, colorful
 53    "nord": Theme("Nord", True, "#2e3440", "#272c36", "#d8dee9", "#3b4252", "#88c0d0", "#2e3440", "#81a1c1",
 54                  "nord"),
 55    "dracula": Theme("Dracula", True, "#282a36", "#21222c", "#f8f8f2", "#44475a", "#bd93f9", "#282a36",
 56                     "#8be9fd", "dracula"),
 57    "solarized-dark": Theme("Solarized Dark", True, "#002b36", "#073642", "#93a1a1", "#0a4352", "#268bd2",
 58                            "#fdf6e3", "#2aa198", "solarized-dark"),
 59    "gruvbox-dark": Theme("Gruvbox Dark", True, "#282828", "#1d2021", "#ebdbb2", "#3c3836", "#d79921", "#282828",
 60                          "#83a598", "gruvbox-dark"),
 61    "one-dark": Theme("One Dark", True, "#282c34", "#21252b", "#abb2bf", "#323842", "#61afef", "#282c34",
 62                      "#56b6c2", "one-dark"),
 63    # Light, colorful
 64    "solarized-light": Theme("Solarized Light", False, "#eee8d5", "#fdf6e3", "#586e75", "#e4ddc8", "#268bd2",
 65                             "#fdf6e3", "#2aa198", "solarized-light"),
 66    "gruvbox-light": Theme("Gruvbox Light", False, "#ebdbb2", "#fbf1c7", "#3c3836", "#d5c4a1", "#b57614",
 67                           "#fbf1c7", "#076678", "gruvbox-light"),
 68    "ocean": Theme("Ocean", False, "#e3eef9", "#f7fbff", "#1b2a3a", "#d0e2f4", "#2f7fd8", "#ffffff", "#1f6fbf",
 69                   "friendly"),
 70    "mint": Theme("Mint", False, "#e6f4ea", "#f8fffa", "#1f3326", "#d2eadb", "#2f9e5c", "#ffffff", "#1d7a47",
 71                  "tango"),
 72}
 73
 74class StillStyle(QProxyStyle):
 75    """A style (the platform's, or Fusion) whose widgets don't animate their
 76    geometry. Even with the main window's AnimatedDocks off, Qt moves panels
 77    with a zero-length animation whenever the style has an animation
 78    duration; on macOS each one's end could re-lay out the panels' tab bars,
 79    which started another, forever (a frozen window at 100% CPU). With no
 80    duration, Qt moves them in one go."""
 81
 82    def styleHint(self, hint, option=None, widget=None, data=None):
 83        if hint == QStyle.SH_Widget_Animation_Duration:
 84            return 0
 85        return super().styleHint(hint, option, widget, data)
 86
 87
 88def style_name(style: QStyle) -> str:
 89    """The name of a style, or of the one a `StillStyle` wraps."""
 90    return (style.baseStyle() if isinstance(style, QProxyStyle) else style).name()
 91
 92
 93current = SYSTEM   # the theme in use (see apply)
 94_native_style = ""  # the platform's style ("macOS", "Windows", …), to go back to from Fusion
 95AUTOMATIC = ""     # View → Window Style: the theme's (the platform's, or Fusion for a colorful theme)
 96chosen_style = AUTOMATIC  # View → Window Style (see window_style)
 97PALETTE_STYLES = {"fusion", "windows"}  # (styles that draw with a colorful theme's palette)
 98
 99
100def styles() -> list[str]:
101    """Qt's styles on this platform (always Fusion)."""
102    return list(QStyleFactory.keys())
103
104
105def native_style() -> str:
106    """The platform's own style (its name as Qt has it)."""
107    return _native_style or style_name(QApplication.instance().style())
108
109
110def window_style(theme: Theme) -> str:
111    """The style to use with a theme: the one chosen in View → Window Style
112    if it's here (a setting from another platform may not be), else the
113    platform's for System / Light / Dark and Fusion for a colorful theme."""
114    available = {name.lower(): name for name in styles()}
115    if chosen_style and chosen_style.lower() in available:
116        return available[chosen_style.lower()]
117    return native_style() if theme.native else "Fusion"
118
119
120def set_window_style(name: str):
121    """View → Window Style (AUTOMATIC: the theme's), applied now."""
122    global chosen_style
123    chosen_style = name
124    apply(current)
125
126
127def palette(theme: Theme) -> QPalette:
128    """A full palette from a theme's few colors."""
129    window, base, text = QColor(theme.window), QColor(theme.base), QColor(theme.text)
130    button, highlight = QColor(theme.button), QColor(theme.highlight)
131    p = QPalette()
132    for group in (QPalette.Active, QPalette.Inactive, QPalette.Disabled):
133        p.setColor(group, QPalette.Window, window)
134        p.setColor(group, QPalette.Base, base)
135        p.setColor(group, QPalette.AlternateBase, base.lighter(112) if theme.dark else base.darker(104))
136        p.setColor(group, QPalette.Button, button)
137        p.setColor(group, QPalette.ToolTipBase, button)
138        p.setColor(group, QPalette.Highlight, highlight)
139        p.setColor(group, QPalette.HighlightedText, QColor(theme.highlighted_text))
140        p.setColor(group, QPalette.Link, QColor(theme.link))
141        p.setColor(group, QPalette.LinkVisited, QColor(theme.link).darker(120))
142        p.setColor(group, QPalette.BrightText, QColor("#ff6b6b"))
143        # Shades for frames and bevels, around the button color.
144        p.setColor(group, QPalette.Light, button.lighter(130))
145        p.setColor(group, QPalette.Midlight, button.lighter(115))
146        p.setColor(group, QPalette.Mid, button.darker(130))
147        p.setColor(group, QPalette.Dark, button.darker(160))
148        p.setColor(group, QPalette.Shadow, QColor(0, 0, 0, 160))
149        muted = QColor(text)
150        muted.setAlpha(140)
151        p.setColor(group, QPalette.PlaceholderText, muted)
152        faint = text if group != QPalette.Disabled else muted
153        for role in (QPalette.WindowText, QPalette.Text, QPalette.ButtonText, QPalette.ToolTipText):
154            p.setColor(group, role, faint)
155    p.setColor(QPalette.Inactive, QPalette.Highlight, highlight.darker(115) if theme.dark else highlight.lighter(115))
156    return p
157
158
159def apply(key: str):
160    """Use a theme for the whole app (an unknown key: System). Qt then sends
161    every window a palette change, which the app's panels follow."""
162    global current, _native_style
163    app = QApplication.instance()
164    theme = THEMES.get(key) or THEMES[SYSTEM]
165    current = key if key in THEMES else SYSTEM
166    if not _native_style:
167        _native_style = style_name(app.style())
168    hints = app.styleHints()
169    if hasattr(hints, "setColorScheme"):  # (Qt 6.8+)
170        scheme = Qt.ColorScheme.Unknown if current == SYSTEM else \
171            Qt.ColorScheme.Dark if theme.dark else Qt.ColorScheme.Light
172        hints.setColorScheme(scheme)
173    wanted = window_style(theme)
174    if not isinstance(app.style(), StillStyle) or style_name(app.style()).lower() != wanted.lower():
175        app.setStyle(StillStyle(wanted))
176        _unpolish_tab_bars(app)
177    app.setPalette(QPalette() if theme.native else palette(theme))  # (native: the platform's, for the scheme)
178
179
180def _unpolish_tab_bars(app: QApplication):
181    """The macOS style gives a document-mode tab bar (the editor's tabs, the
182    panels') a near-black text color and a small font of its own when it
183    polishes it. At startup the app's font and palette replace them; after a
184    theme's change of style (Fusion → macOS) nothing did: dark text on a dark
185    bar, and names cut off (the tabs measured in the small font). Back to the
186    app's, as at startup."""
187    for widget in app.allWidgets():
188        if isinstance(widget, QTabBar) and widget.documentMode():
189            widget.setPalette(QPalette())
190            widget.setFont(QFont())
191
192
193def syntax_style() -> str:
194    """The Pygments style the current theme goes with ("" for none)."""
195    return THEMES.get(current, THEMES[SYSTEM]).syntax
class Theme(typing.NamedTuple):
30class Theme(NamedTuple):
31    title: str
32    dark: bool
33    window: str = ""        # "" for the platform's own (System, Light, Dark)
34    base: str = ""          # text fields, lists, the editor's surroundings
35    text: str = ""
36    button: str = ""
37    highlight: str = ""     # selection and accents
38    highlighted_text: str = ""
39    link: str = ""
40    syntax: str = ""        # the Pygments style for Syntax Colors → Automatic ("": light/dark default)
41
42    @property
43    def native(self) -> bool:
44        """The platform's look (only light or dark chosen), not our palette."""
45        return not self.window

Theme(title, dark, window, base, text, button, highlight, highlighted_text, link, syntax)

Theme( title: str, dark: bool, window: str = '', base: str = '', text: str = '', button: str = '', highlight: str = '', highlighted_text: str = '', link: str = '', syntax: str = '')

Create new instance of Theme(title, dark, window, base, text, button, highlight, highlighted_text, link, syntax)

title: str

Alias for field number 0

dark: bool

Alias for field number 1

window: str

Alias for field number 2

base: str

Alias for field number 3

text: str

Alias for field number 4

button: str

Alias for field number 5

highlight: str

Alias for field number 6

highlighted_text: str

Alias for field number 7

syntax: str

Alias for field number 9

native: bool
42    @property
43    def native(self) -> bool:
44        """The platform's look (only light or dark chosen), not our palette."""
45        return not self.window

The platform's look (only light or dark chosen), not our palette.

SYSTEM = 'system'
THEMES: dict[str, Theme] = {'system': Theme(title='System (follow light/dark mode)', dark=False, window='', base='', text='', button='', highlight='', highlighted_text='', link='', syntax=''), 'light': Theme(title='Light', dark=False, window='', base='', text='', button='', highlight='', highlighted_text='', link='', syntax=''), 'dark': Theme(title='Dark', dark=True, window='', base='', text='', button='', highlight='', highlighted_text='', link='', syntax=''), 'nord': Theme(title='Nord', dark=True, window='#2e3440', base='#272c36', text='#d8dee9', button='#3b4252', highlight='#88c0d0', highlighted_text='#2e3440', link='#81a1c1', syntax='nord'), 'dracula': Theme(title='Dracula', dark=True, window='#282a36', base='#21222c', text='#f8f8f2', button='#44475a', highlight='#bd93f9', highlighted_text='#282a36', link='#8be9fd', syntax='dracula'), 'solarized-dark': Theme(title='Solarized Dark', dark=True, window='#002b36', base='#073642', text='#93a1a1', button='#0a4352', highlight='#268bd2', highlighted_text='#fdf6e3', link='#2aa198', syntax='solarized-dark'), 'gruvbox-dark': Theme(title='Gruvbox Dark', dark=True, window='#282828', base='#1d2021', text='#ebdbb2', button='#3c3836', highlight='#d79921', highlighted_text='#282828', link='#83a598', syntax='gruvbox-dark'), 'one-dark': Theme(title='One Dark', dark=True, window='#282c34', base='#21252b', text='#abb2bf', button='#323842', highlight='#61afef', highlighted_text='#282c34', link='#56b6c2', syntax='one-dark'), 'solarized-light': Theme(title='Solarized Light', dark=False, window='#eee8d5', base='#fdf6e3', text='#586e75', button='#e4ddc8', highlight='#268bd2', highlighted_text='#fdf6e3', link='#2aa198', syntax='solarized-light'), 'gruvbox-light': Theme(title='Gruvbox Light', dark=False, window='#ebdbb2', base='#fbf1c7', text='#3c3836', button='#d5c4a1', highlight='#b57614', highlighted_text='#fbf1c7', link='#076678', syntax='gruvbox-light'), 'ocean': Theme(title='Ocean', dark=False, window='#e3eef9', base='#f7fbff', text='#1b2a3a', button='#d0e2f4', highlight='#2f7fd8', highlighted_text='#ffffff', link='#1f6fbf', syntax='friendly'), 'mint': Theme(title='Mint', dark=False, window='#e6f4ea', base='#f8fffa', text='#1f3326', button='#d2eadb', highlight='#2f9e5c', highlighted_text='#ffffff', link='#1d7a47', syntax='tango')}
class StillStyle(PySide6.QtWidgets.QProxyStyle):
75class StillStyle(QProxyStyle):
76    """A style (the platform's, or Fusion) whose widgets don't animate their
77    geometry. Even with the main window's AnimatedDocks off, Qt moves panels
78    with a zero-length animation whenever the style has an animation
79    duration; on macOS each one's end could re-lay out the panels' tab bars,
80    which started another, forever (a frozen window at 100% CPU). With no
81    duration, Qt moves them in one go."""
82
83    def styleHint(self, hint, option=None, widget=None, data=None):
84        if hint == QStyle.SH_Widget_Animation_Duration:
85            return 0
86        return super().styleHint(hint, option, widget, data)

A style (the platform's, or Fusion) whose widgets don't animate their geometry. Even with the main window's AnimatedDocks off, Qt moves panels with a zero-length animation whenever the style has an animation duration; on macOS each one's end could re-lay out the panels' tab bars, which started another, forever (a frozen window at 100% CPU). With no duration, Qt moves them in one go.

def styleHint(self, hint, option=None, widget=None, data=None):
83    def styleHint(self, hint, option=None, widget=None, data=None):
84        if hint == QStyle.SH_Widget_Animation_Duration:
85            return 0
86        return super().styleHint(hint, option, widget, data)

styleHint(self, hint: PySide6.QtWidgets.QStyle.StyleHint, /, option: PySide6.QtWidgets.QStyleOption | None = None, widget: PySide6.QtWidgets.QWidget | None = None, returnData: PySide6.QtWidgets.QStyleHintReturn | None = None) -> int

staticMetaObject = PySide6.QtCore.QMetaObject("StillStyle" inherits "QProxyStyle": )
def style_name(style: PySide6.QtWidgets.QStyle) -> str:
89def style_name(style: QStyle) -> str:
90    """The name of a style, or of the one a `StillStyle` wraps."""
91    return (style.baseStyle() if isinstance(style, QProxyStyle) else style).name()

The name of a style, or of the one a StillStyle wraps.

current = 'system'
AUTOMATIC = ''
chosen_style = ''
PALETTE_STYLES = {'fusion', 'windows'}
def styles() -> list[str]:
101def styles() -> list[str]:
102    """Qt's styles on this platform (always Fusion)."""
103    return list(QStyleFactory.keys())

Qt's styles on this platform (always Fusion).

def native_style() -> str:
106def native_style() -> str:
107    """The platform's own style (its name as Qt has it)."""
108    return _native_style or style_name(QApplication.instance().style())

The platform's own style (its name as Qt has it).

def window_style(theme: Theme) -> str:
111def window_style(theme: Theme) -> str:
112    """The style to use with a theme: the one chosen in View → Window Style
113    if it's here (a setting from another platform may not be), else the
114    platform's for System / Light / Dark and Fusion for a colorful theme."""
115    available = {name.lower(): name for name in styles()}
116    if chosen_style and chosen_style.lower() in available:
117        return available[chosen_style.lower()]
118    return native_style() if theme.native else "Fusion"

The style to use with a theme: the one chosen in View → Window Style if it's here (a setting from another platform may not be), else the platform's for System / Light / Dark and Fusion for a colorful theme.

def set_window_style(name: str):
121def set_window_style(name: str):
122    """View → Window Style (AUTOMATIC: the theme's), applied now."""
123    global chosen_style
124    chosen_style = name
125    apply(current)

View → Window Style (AUTOMATIC: the theme's), applied now.

def palette(theme: Theme) -> PySide6.QtGui.QPalette:
128def palette(theme: Theme) -> QPalette:
129    """A full palette from a theme's few colors."""
130    window, base, text = QColor(theme.window), QColor(theme.base), QColor(theme.text)
131    button, highlight = QColor(theme.button), QColor(theme.highlight)
132    p = QPalette()
133    for group in (QPalette.Active, QPalette.Inactive, QPalette.Disabled):
134        p.setColor(group, QPalette.Window, window)
135        p.setColor(group, QPalette.Base, base)
136        p.setColor(group, QPalette.AlternateBase, base.lighter(112) if theme.dark else base.darker(104))
137        p.setColor(group, QPalette.Button, button)
138        p.setColor(group, QPalette.ToolTipBase, button)
139        p.setColor(group, QPalette.Highlight, highlight)
140        p.setColor(group, QPalette.HighlightedText, QColor(theme.highlighted_text))
141        p.setColor(group, QPalette.Link, QColor(theme.link))
142        p.setColor(group, QPalette.LinkVisited, QColor(theme.link).darker(120))
143        p.setColor(group, QPalette.BrightText, QColor("#ff6b6b"))
144        # Shades for frames and bevels, around the button color.
145        p.setColor(group, QPalette.Light, button.lighter(130))
146        p.setColor(group, QPalette.Midlight, button.lighter(115))
147        p.setColor(group, QPalette.Mid, button.darker(130))
148        p.setColor(group, QPalette.Dark, button.darker(160))
149        p.setColor(group, QPalette.Shadow, QColor(0, 0, 0, 160))
150        muted = QColor(text)
151        muted.setAlpha(140)
152        p.setColor(group, QPalette.PlaceholderText, muted)
153        faint = text if group != QPalette.Disabled else muted
154        for role in (QPalette.WindowText, QPalette.Text, QPalette.ButtonText, QPalette.ToolTipText):
155            p.setColor(group, role, faint)
156    p.setColor(QPalette.Inactive, QPalette.Highlight, highlight.darker(115) if theme.dark else highlight.lighter(115))
157    return p

A full palette from a theme's few colors.

def apply(key: str):
160def apply(key: str):
161    """Use a theme for the whole app (an unknown key: System). Qt then sends
162    every window a palette change, which the app's panels follow."""
163    global current, _native_style
164    app = QApplication.instance()
165    theme = THEMES.get(key) or THEMES[SYSTEM]
166    current = key if key in THEMES else SYSTEM
167    if not _native_style:
168        _native_style = style_name(app.style())
169    hints = app.styleHints()
170    if hasattr(hints, "setColorScheme"):  # (Qt 6.8+)
171        scheme = Qt.ColorScheme.Unknown if current == SYSTEM else \
172            Qt.ColorScheme.Dark if theme.dark else Qt.ColorScheme.Light
173        hints.setColorScheme(scheme)
174    wanted = window_style(theme)
175    if not isinstance(app.style(), StillStyle) or style_name(app.style()).lower() != wanted.lower():
176        app.setStyle(StillStyle(wanted))
177        _unpolish_tab_bars(app)
178    app.setPalette(QPalette() if theme.native else palette(theme))  # (native: the platform's, for the scheme)

Use a theme for the whole app (an unknown key: System). Qt then sends every window a palette change, which the app's panels follow.

def syntax_style() -> str:
194def syntax_style() -> str:
195    """The Pygments style the current theme goes with ("" for none)."""
196    return THEMES.get(current, THEMES[SYSTEM]).syntax

The Pygments style the current theme goes with ("" for none).