[PATCH 0 of 8 DarkThemeSeries 1] dark theme support for TortoiseHg

16 views
Skip to first unread message

Peter Demcak

unread,
Mar 7, 2026, 1:28:25 PM (3 days ago) Mar 7
to thg...@googlegroups.com
This patch series introduces dark theme support for TortoiseHg, including six built-in dark themes and support for custom user-defined themes.

The original project and older screenshots are available here:
https://github.com/majster64/tortoisehg-dark-themes

Scope:

- 24 commits in 3 patch series (to be emailed over 3 days)
- 34 modified files, 1 new file
- ~1700 lines of code added

Tested on Windows 10/11 and Ubuntu 22.04 with both PyQt5 and PyQt6.
The code path remains unchanged when the dark theme is not enabled via Settings / Workbench / Theme.
Dark theme is applied to all windows and widgets.
No known bugs.

Peter Demcak

unread,
Mar 7, 2026, 1:28:32 PM (3 days ago) Mar 7
to thg...@googlegroups.com
# HG changeset patch
# User Peter Demcak <majs...@gmail.com>
# Date 1772736652 -3600
# Thu Mar 05 19:50:52 2026 +0100
# Node ID b7b298c0720ca349433dcb2ad2a5a50ef4d7679b
# Parent 2c863d5585ab0c7f26addedfbc5aceecab568240
qtlib: add PyQt5/PyQt6 compatibility enum wrappers

PyQt5 exposes class-level enums as flat attributes (e.g. QPalette.Window),
while PyQt6 uses scoped enums (e.g. QPalette.ColorRole.Window). This adds
IntEnum wrappers so the rest of the codebase can use consistent names
regardless of the Qt version:

- QtPaletteRole, QtPaletteGroup (QPalette.ColorRole / ColorGroup)
- QtStateFlag (QStyle.StateFlag, wrapped as ints for bitwise ops in PyQt6)
- QtItemDataRole (Qt.ItemDataRole)
- QtPainterRenderHint (QPainter.RenderHint)

diff -r 2c863d5585ab -r b7b298c0720c tortoisehg/hgqt/qtlib.py
--- a/tortoisehg/hgqt/qtlib.py Thu Mar 05 19:44:24 2026 +0100
+++ b/tortoisehg/hgqt/qtlib.py Thu Mar 05 19:50:52 2026 +0100
@@ -119,6 +119,74 @@
except ImportError:
openflags = 0

+# Qt5/Qt6 compatibility:
+# This section creates class-level enums so code can use consistent names regardless of the Qt version.
+from enum import IntEnum
+
+if QT_API == 'PyQt6':
+ from PyQt6.QtCore import Qt as _QtCore_Qt
+ from PyQt6.QtGui import QPalette as _QPalette_cls, QPainter as _QPainter_cls
+ from PyQt6.QtWidgets import QStyle as _QStyle_cls
+
+ # QStyle StateFlag wrapped as ints so bitwise ops work in PyQt6
+ class QtStateFlag:
+ State_Selected = int(_QStyle_cls.StateFlag.State_Selected.value)
+ State_Enabled = int(_QStyle_cls.StateFlag.State_Enabled.value)
+ State_Active = int(_QStyle_cls.StateFlag.State_Active.value)
+ State_On = int(_QStyle_cls.StateFlag.State_On.value)
+ State_Off = int(_QStyle_cls.StateFlag.State_Off.value)
+
+ QtPaletteRole = _QPalette_cls.ColorRole
+ QtPaletteGroup = _QPalette_cls.ColorGroup
+ QtItemDataRole = _QtCore_Qt.ItemDataRole
+ QtPainterRenderHint = _QPainter_cls.RenderHint
+
+else: # PyQt5
+ from .qtcore import Qt as _QtCore
+ from .qtgui import QStyle as _QStyle, QPainter as _QPainter, QPalette as _QPalette
+
+ class QtPaletteRole(IntEnum):
+ Window = _QPalette.Window
+ Base = _QPalette.Base
+ AlternateBase = _QPalette.AlternateBase
+ Text = _QPalette.Text
+ WindowText = _QPalette.WindowText
+ Mid = _QPalette.Mid
+ Dark = _QPalette.Dark
+ Light = _QPalette.Light
+ Highlight = _QPalette.Highlight
+ HighlightedText = _QPalette.HighlightedText
+ Link = _QPalette.Link
+ LinkVisited = _QPalette.LinkVisited
+
+ class QtPaletteGroup(IntEnum):
+ Active = _QPalette.Active
+ Inactive = _QPalette.Inactive
+ Disabled = _QPalette.Disabled
+
+ class QtStateFlag(IntEnum):
+ State_Selected = _QStyle.State_Selected
+ State_Enabled = _QStyle.State_Enabled
+ State_Active = _QStyle.State_Active
+ State_On = _QStyle.State_On
+ State_Off = _QStyle.State_Off
+
+ class QtItemDataRole(IntEnum):
+ ForegroundRole = _QtCore.ForegroundRole
+ BackgroundRole = _QtCore.BackgroundRole
+ DisplayRole = _QtCore.DisplayRole
+ DecorationRole = _QtCore.DecorationRole
+ ToolTipRole = _QtCore.ToolTipRole
+
+ class QtPainterRenderHint(IntEnum):
+ Antialiasing = _QPainter.Antialiasing
+ SmoothPixmapTransform = _QPainter.SmoothPixmapTransform
+ TextAntialiasing = _QPainter.TextAntialiasing
+
+def stateValue(state):
+ """Extract raw int from a QStyle.StateFlag (works on both PyQt5 and PyQt6)."""
+ return state.value if hasattr(state, 'value') else int(state)
+
_W = TypeVar('_W', bound=QWidget)

# def htmlescape(s: Text, quote: bool = True) -> Text:

Yuya Nishihara

unread,
Mar 8, 2026, 7:30:03 AM (2 days ago) Mar 8
to Peter Demcak, thg...@googlegroups.com
Thanks for the summary.

First thing first, I don't think we would want to maintain multiple
themes and custom styles of the common QWidgets. I believe we don't have
maintainer resources for that kind of works.

The base QWidget styles can be customized by platform themes and tools
like qt6ct.

We have some hard-coded colors (mainly around QScintilla) that wouldn't
work on dark-color environment. I think this problem can be fixed by
adding alternative colors for dark themes. If you want to customize
some of them, add config knobs instead of defining hardcoded theme
colors.

Thanks.

Peter Demcak

unread,
Mar 9, 2026, 9:57:25 AM (20 hours ago) Mar 9
to TortoiseHg Developers
Thanks for the feedback.

My main concern is that relying on external tools and manual configuration may not be very convenient for many users.
Setting up a custom Qt theme (for example with tools like qt6ct) can take some time.

In this patch series, a significant part of the work (about 14 out of the 24 commits) is fixing hard-coded colors in TortoiseHg itself,
mainly around QScintilla/lexer components. Another 4 commits are related to Qt5/Qt6 compatibility.

However, the dark themes in my GitHub project are easier for users to install.
It currently provides six themes (two saturated, two mild, and two dim themes), and the setup only takes about a minute.

If integrating built-in themes is not something the project wants to support for now, I'm perfectly fine keeping the project maintained separately on GitHub.
If the project ever becomes interested in built-in theme support in the future, I’d be happy to help revisit this work.
Reply all
Reply to author
Forward
0 new messages